I have a hard typing problem with typing my function use Typescript:
Args:
template: ['a', 'b', 'b']
arrays: {
a: string[],
b: number[],
}
Result:
type = (string | number)[]
But I want to get a tuple type
type = [string, number, number]
// because item of array with key 'a' has type string, 'b' - number
How I can make it?
I know how I can get a array items type:
type GetArrayItemType<Items extends any[]> = Items extends (infer Item)[] ? Item : never;
And how I can put this types on object keys:
type ArrayElements<Arrays extends Record<string, unknown[]>> = {
[Key in keyof Arrays]: Arrays[Key][number];
};
But I dont know how I can make a tuple with iterating on keys template argument
You can map tuple types to other tuple types, so I'd start with the tuple type from template
rather than the object type from arrays
, like this:
type Mapper<Tuple extends any[], Source extends Record<string, any[]>> = {
[Index in keyof Tuple]: Source[Tuple[Index]][number];
};
type Result = Mapper<typeof template, typeof arrays>;
// ^? −−− type Result = [string, number, number]
I've used typeof
on template
and arrays
because the way they're written in the question, they look like identifiers with type annotations on them, not types, but remove that if they're actually types.
In a comment you've said:
I can't use
template
as type, because it is a function argument which can be a some string array:fn<ArrayKey extends string>(template: ArrayKey[], arrays: Record<ArrayKey, any[]>)
And I want to get dynamic result type:fn(['a', 'b'], { a: ['s', 1], b: [null] }) => [string | number, null]
orfn(['key1', 'key2', 'key1'], { key1: [1, 2, 3], key2: [true, 4] }) => [number, boolean | number, number]
You can do that with the Mapper
type above provided the template arrays you're passing into fn
are tuples of string literals, not just arrays of strings. That is, TypeScript has to know the string literal types of the elements. In your example calls, they're just arrays, but we can make them work by adding as const
to them (or by defining them separately and applying a type annotation to them). Here's an initial definition for fn
, but we'll revisit it in a moment:
function fn<Template extends any[], Arrays extends Record<Template[number], any[]>>(
template: Template,
arrays: Arrays
): Mapper<Template, Arrays> {
// Insert implementation
}
Then the calls work with the added as const
:
const r1 = fn(["a", "b"] as const, { a: ["s", 1], b: [null] });
// ^? const r1: [string | number, null]
const r2 = fn(["key1", "key2", "key1"] as const, { key1: [1, 2, 3], key2: [true, 4] });
// ^? const r2: [number, number | boolean, number]
...and with a separate tuple with a type annotation:
const a3: ["a", "b"] = ["a", "b"];
const r3 = fn(a3, { a: ["s", 1], b: [null] });
// ^? const r3: [string | number, null]
That's good, but it wouldn't work if the tuples were defined separately with as const
like this:
const a4 = ["a", "b"] as const;
That creates a readonly
tuple, which isn't compatible with our any[]
type constraint on Template
. Since that's a fairly common pattern, we probably want to support that. We can support it by adding Readonly<>
to the type constraint, and removing readonly
from the type Mapper
creates using the -readonly
modifier (unless you want to keep it). Here's the updated Mapper
and function definition:
type Mapper<Tuple extends Readonly<any[]>, Source extends Record<string, any[]>> = {
-readonly [Index in keyof Tuple]: Source[Tuple[Index]][number];
};
function fn<Template extends Readonly<any[]>, Arrays extends Record<Template[number], any[]>>(
template: Template,
arrays: Arrays
): Mapper<Template, Arrays> {
// Insert implementation
}
Then all the various forms of calls above work:
const r1 = fn(["a", "b"] as const, { a: ["s", 1], b: [null] });
// ^? const r1: [string | number, null]
const r2 = fn(["key1", "key2", "key1"] as const, { key1: [1, 2, 3], key2: [true, 4] });
// ^? const r2: [number, number | boolean, number]
const a3: ["a", "b"] = ["a", "b"];
const r3 = fn(a3, { a: ["s", 1], b: [null] });
// ^? const r3: [string | number, null]
const a4 = ["a", "b"] as const;
const r4 = fn(a4, { a: ["s", 1], b: [null] });
// ^? const r4: [string | number, null]