Typescript Typings: массив T для отображения - PullRequest
1 голос
/ 10 марта 2019

Допустим, у нас есть тип T:

type T = {
  type: string,
}

и функция, которая принимает массив T и возвращает объект, ключи которого являются значениями каждого T.type, а значения - T

const toMap = (...args: T[]) => args.reduce((res, t) => ({
  ...res,
  [t.type]: t
}), {});

Итак, для данного примера:

const a = { type: 'hello' };
const b = { type: 'world' };
const c = { type: 'foo' };

const map = toMap(a, b, c);

Я ожидаю этого результата

{
  hello: { type: 'hello' },
  world: { type: 'world' },
  foo: { type: 'foo' },
}

map.hello // correct, { type: 'hello' };

// If I access an unknown property, then the compiler should: 
map.bar // `property bar doesn't exist on type { hello: { ... }, world: {...}, foo: {...} }`

как я могу написать наборы для этой функции?

1 Ответ

2 голосов
/ 10 марта 2019

Вы можете начать с T действительно универсальным:

function toMap<T extends { type: string }>(...args: T[]): { [type: string]: T } {
  return args.reduce((res, t) => ({
    ...res,
   [t.type]: t
  }), {});
}

Чтобы затем иметь возможность действительно сузить типы, вы должны набирать универсальные типы для переменных аргументов, например toMap<A>(arg1: A), toMap<A, B>(arg1: A, arg2: B).

Есть два недостатка:

1) Вы должны создать эти перегрузки для любого количества аргументов, однако это часто встречается в Typescript (см. Object.assign объявление).

2) Typescript по умолчанию { type: "test" } используется как { type: string } (что требуется в 99% случаев), однако, поэтому мы не можем напрямую выводить тип клавиш на "test". Чтобы решить эту проблему, мы должны типизировать строковый литерал к суженному строковому типу { type: "test" as "test" }.

// generic overload for one argument
function toMap<A>(arg: A): { [K1 in O<A>]: A };

// generic overload for two arguments:
function toMap<A, B>(arg: A, arg2: B): { [K in O<A>]: A } & { [K in O<B>]: B };

// generic overload for three arguments:
function toMap<A, B, C>(arg: A, arg2: B, arg3: C): { [K in O<A>]: A } & { [K in O<B>]: B } & { [K in O<C>]: C };

// ... repeat for more arguments

// implementation for all kind of args
function toMap<T extends { type: string }>(...args: T[]): { [type: string]: T } {
   return args.reduce((res, t) => ({
     ...res,
    [t.type]: t
  }), {});
}

// Infers the type of "type", which has to be a string, from a given object
type O<V> = V extends { type: infer K } ? K extends string ? K : never : never;

// Narrow down a.type to be "test" instead of string
const a = { type: "test" as "test" }
const b = { type: "test2" as "test2", v: 1 };

const test = toMap(a);
const test2 = toMap(a, b);

console.log(
 test2.test2.v, // works!
 test2.whatever, // doesnt!
 test2.test2.k // doesnt!
);

Попробуйте!

...