Вы можете начать с 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!
);
Попробуйте!