Search code examples
typescript

How to return tuple type from object of some types arrays and object keys array template?


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


Solution

  • 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]
    

    Playground link

    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] or fn(['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]
    

    Playground link

    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]
    

    Playground link