Home > Mobile >  Typescript Mapped Type. Resulting property type in is not calculated if mix of Generic used
Typescript Mapped Type. Resulting property type in is not calculated if mix of Generic used

Time:11-26

I try to build generic method that accepts Mapped Type called QueryParamObject and partially reads its properties:

QueryParamObject accepts any type and results to a type where all properties are either string[] or string:

export type QueryParamObject<TInput> = {
    [key in keyof TInput]: TInput[key] extends Array<string | number> ? string[] : string;
};

It works perfect when I use concrete types:

type Concrete = {
    numberProp: number;
    numberArray: number[];
    stringArray: string[];
}

function read(arg: QueryParamObject<Concrete>): void {
    const arr1: string[] = arg.numberArray;
    const prop1: string = arg.numberProp;

    // const arr2: number[] = arg.numberArray; //will have type error
}

But everything breaks and resulting type of each property becomes string[] | string when I add a Generic:

function read<TExtra>(arg: QueryParamObject<TExtra & Concrete>): void {
    const arr1: string[] = arg.numberArray; //Type 'string | string[]' is not assignable to type 'string[]'
}

When using a Generic type compiler cannot calculate the resulting type properly

Example

What is wrong with this notation? How can I make Typescript to properly calculate those properties even if I use Generics?

CodePudding user response:

QueryParamObject takes an intersection of a generic type TExtra and a concrete type Concrete. Whenever you use generic types to compute other types using mapped types or conditionals, you can't rely on the compiler to fully reason about the high level implications of those types.

In fact, most of the times, the compiler will not try to compute the result of those generic types at all, leaving us with opaque types which are often quite useless. Now given this example with a mapped type, we can see that the compiler is smart enough to not leave the type totally opaque as it at least understands that a property of QueryParamObject can be either string[] | number.

To have a satisfying conclusion, I would recommend to move the intersection.

function read<TExtra>(
  arg: QueryParamObject<Concrete> & QueryParamObject<TExtra>
): void {
    const arr1: string[] = arg.numberArray;
}

The compiler can now fully compute and understand QueryParamObject<Concrete> which allows accessing the three properties with the correct type. The intersection with QueryParamObject<TExtra> remains as a type constraint to the caller of the function.


Playground

  • Related