如何在TypeScript中将对象键的值映射到同一对象?

时间:2020-10-10 17:38:42

标签: typescript typescript-typings

如何以IntelliSense知道工作的方式将对象键的值映射到TypeScript中的同一对象,如下例所示?

setStretchLastSection

3 个答案:

答案 0 :(得分:3)

您可以执行以下操作:

export function getByName<
  TName extends string,
  TObj extends {__name: TName},
>(arr: TObj[]): Record<TName, TObj> {
  return arr.reduce((acc, obj) => {
    return {
      ...acc,
      [obj.__name]: obj,
    };
  }, {} as Partial<Record<TName, TObj>>) as Record<TName, TObj>;
}

typescript playground

编辑:上面的解决方案有两个问题:

  1. 生成的对象具有任何string的键(表示obj.nonExistingKey.qux将通过)
  2. 尽管提供的数组中没有obj.foo.qux,它仍认为{__name: 'foo', qux: 'value'}没问题。

要解决问题1,我们可以传递数组as const

function getByName<TObj extends {__name: string}>(arr: readonly TObj[]) {
  return arr.reduce((acc, obj) => {
    return {
      ...acc,
      [obj.__name]: obj,
    };
  }, {}) as Record<TObj['__name'], TObj>;
}

const obj = getByName([
  { __name: 'foo', baz: 'foobar' },
  { __name: 'bar', qux: 'quux' },
] as const);

obj.nonExistingKey.quuz // not ok NOW!

typescript playground

但是,我不知道泛型问题2的解决方案。如果这对您确实很重要,则可以实施some type guards

答案 1 :(得分:2)

棘手的部分是尝试维护属性名称和该属性的特定接口之间的关系。理想情况下,属性baz将存在于键foo上,而不存在于键bar上。

我知道它是半工作的,但是只有当您使用__name: 'foo' as const表示此名称的类型仅是文字字符串'foo'时,它才有效。否则,打字稿会将每个名称的类型视为string,并且特定名称和特定属性之间的关联会丢失。

// standard util to get element type of an array 
type Unpack<T> = T extends (infer U)[] ? U : never;

type KeyedByName<U extends {__name: string}[]> = {
    [K in Unpack<U>['__name']]: Extract<Unpack<U>, {__name: K}>
}

KeyedByName中,我们说一个键的值为只能是其__name属性与该键的类型匹配的数组的元素。但是,如果密钥类型仅为string,则根本不会缩小。

如果我们使用'foo' as const表示法,则返回类型KeyedByName变得非常具体。

const inputsConst = [
  { __name: 'foo' as const, baz: 'foobar' },
  { __name: 'bar' as const, qux: 'quux' },
];

type K1 = KeyedByName<typeof inputsConst>

评估为

type K1 = {
    foo: {
        __name: "foo";
        baz: string;
        qux?: undefined;
    };
    bar: {
        __name: "bar";
        qux: string;
        baz?: undefined;
    };
}

我们知道某些属性是必需的,而其他属性则不存在(只能是undefined)。

const checkK1 = ( obj: K1 ) => {

    const fooName: string = obj.foo.__name // ok
    const fooBaz: string = obj.foo.baz // required to be string
    const fooQux: undefined = obj.foo.qux // can access, but will always be undefined because it doesn't exist
    const fooQuuz = obj.foo.quuz // error

    const barName: string = obj.bar.__name // ok
    const barQux: string = obj.bar.qux // required to be string
    const barBaz: undefined = obj.bar.baz // can access, but will always be undefined because it doesn't exist
    const barQuuz = obj.bar.quuz // error
}

但是,在不使用foo as const的情况下,此类型与@gurisko的答案中的Record相比并没有更具体,因为打字稿将'foo''bar'的类型都视为{{ 1}},因此它们是等效的。

string

评估为:

const inputsPlain = [
    { __name: 'foo', baz: 'foobar' },
    { __name: 'bar', qux: 'quux' },
];

type K2 = KeyedByName<typeof inputsPlain>

所有属性都视为可选属性,无论它们来自type K2 = { [x: string]: { __name: string; baz: string; qux?: undefined; } | { __name: string; qux: string; baz?: undefined; }; } 还是foo

bar

Playground Link

答案 2 :(得分:1)

以下答案很大程度上基于@Linda Paiste's answer

区别在于,我不是在使用普通对象,而是在使用类作为替代对象。引用琳达:

KeyedByName 中,我们说一个键的值只能是其__name属性与该键的类型匹配的数组元素。但是,如果键类型只是字符串,则根本不会缩小。

我相信通过使用类,TypeScript在解压缩类型时会采用一种更有效的方法来推断类型,因为它似乎使用映射到类本身的__types属性对对象进行类型检查。它没有将简单的对象组合为一个对象,而是有效地推断出正确的类型,从而导致KeyedByName解决以下问题:

class Foo {
  get __name(): 'foo' { return 'foo'; }

  constructor(public baz: string) {}
}

class Bar {
  public readonly __name: 'bar' = 'bar';

  constructor(public qux: string) {}
}

type ObjectsByName = KeyedByName<typeof inputs>;

ObjectsByName 的计算结果为:

type ObjectsByName = {
    foo: Foo;
    bar: Bar;
}

Playground Link