I want to write a type-safe function that takes an argument representing a prop that is a primitive or maybe a more specific object with values of the same primitive. for example t=number
or t={x:number}
or t={x:number,y:number}
so for example prop named grid which is number
but can be specified with horizontal|vertical
fields, so valid input would be grid=10
or grid={horizontal:10}
or grid={vertical:10}
or grid={horizontal:10,vertical:10}
a working implementation:
export type Primitive = string | number | boolean | null | undefined;
export type PossiblySpecifyAxis<T> = T | SpecifyAxis<T>;
export type SpecifyAxis<T> = { horizontal?: T; vertical?: T };
const parsePossiblyAxis = <
Prop extends PossiblySpecifyAxis<Primitive>,
Default extends NonNullable<Primitive> | undefined = undefined
>(
prop: Prop,
defaultValue?: Default
): {
horizontal: Prop extends Primitive ? Prop : Prop extends { horizontal: infer V } ? V : Default;
vertical: Prop extends Primitive ? Prop : Prop extends { vertical: infer V } ? V : Default;
} => {
// does not matter
/////////////////////////////////////////
return {} as any
};
// works great
const c1 = parsePossiblyAxis(10) // => {horizontal: 10; vertical: 10;}
const c2 = parsePossiblyAxis({horizontal:5} as const,10) // => {horizontal: 5; vertical: 10;}
const c3 = parsePossiblyAxis(10,30) // => {horizontal: 10; vertical: 10;}
Now i want to make this function more generic, so instead of specifying horizontal|vertical
hardcoded i would like to except this type as a generic as well:
export type Primitive = string | number | boolean | null | undefined;
export type PossiblySpecific<Prop, Spec extends string> = Prop | { [key in Spec]: Prop };
//example: type t1 = PossiblySpecific<10,'x'|'y'> // => '10' | {x:10,y:10}
const parsePossiblySpecific = <
Spec extends string[],
Prop extends PossiblySpecific<Primitive, Spec[number]>,
Default extends NonNullable<Primitive> | undefined = undefined
>(
prop: Prop,
fields: Spec,
defaultValue?: Default
): {
////instead of
// horizontal: Prop extends Primitive ? Prop : Prop extends { horizontal: infer V } ? V : Default;
// vertical: Prop extends Primitive ? Prop : Prop extends { vertical: infer V } ? V : Default;
//// use 'Spec' generic to infer the type of the fields and construct the return type
[key in keyof Spec]: Prop extends Primitive ? Prop : Prop extends { [key in keyof Spec]: infer V } ? V : Default;
} => {
// doesn't matter
}
};
const c1 = parsePossiblySpecific(10,["horizontal","vertical"]) // => 10[] => Wrong! why this is the resulted type??
In simple words: i am asking: why does:
// works pefectly fine: object is returned
{
horizontal: Prop extends Primitive ? Prop : Prop extends { horizontal: infer V } ? V : Default;
vertical: Prop extends Primitive ? Prop : Prop extends { vertical: infer V } ? V : Default;
}
// returns an array - WHY?
{
[key in keyof Spec]: Prop extends Primitive ? Prop : Prop extends { [key in keyof Spec]: infer V } ? V : Default;
}
to see full working examples see ts-playgrounds. the js works great but the inferred type is wrong. this should be a stupid mistake but I can't find it. thanks in advance
CodePudding user response:
The first thing I would change is the return type of the function. Since Spec
is a tuple, mapping over it will also result in a tuple. Instead you could map over the union of the contents of Spec.
{
[key in Spec[number]]: Prop extends Primitive
? Prop
: Prop extends { [key in keyof Spec]: infer V }
? V
: Default;
}
The other problem is the inference of Spec
. The way you specified the argument fields
, Spec
will always be infered as just string[]
instead of a tuple like ["horizontal","vertical"]
.
To fix this, we can use the spread syntax.
const parsePossiblySpecific = <
Spec extends string[],
Prop extends PossiblySpecific<Primitive, Spec[number]>,
Default extends NonNullable<Primitive> | undefined = undefined
>(
prop: Prop,
fields: [...Spec],
defaultValue?: Default
)