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[]'
}
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.