I want to write a type 'PROP' for this to work
let a : PROP = {
type: String,
default: 'STR'
} // OK
let a : PROP = {
type: String,
default: []
} // ERR
In general, a type in which the value of the default field will depend on the value of the field type. I tried to write
type CleanPropTypes = typeof Array | typeof Object | typeof Function | typeof Boolean | typeof String
type PROP<T = CleanPropTypes, U = any> = {
type:T,
default?: U extends T
}
var b : PROP = {
type:Array,
default:[]
}
example
But it doesn't work. How to write this type ?
The right definition for Prop
should probably be a union of valid type
/default
pairs corresponding to each primitive wrapper object creator in CleanPropTypes
. Something like this:
type Prop = {
type: ArrayConstructor;
default?: unknown[] | undefined;
} | {
type: ObjectConstructor;
default?: object | undefined;
} | {
type: FunctionConstructor;
default?: Function | undefined;
} | {
type: BooleanConstructor;
default?: boolean | undefined;
} | {
type: StringConstructor;
default?: string | undefined;
}
That behaves how you'd like:
let good1: Prop = {
type: String,
default: 'STR'
} // okay
let bad1: Prop = {
type: String,
default: []
} // error
var good2: Prop = {
type: Array,
default: []
} // okay
var bad2: Prop = {
type: Object,
default: "oops"
} // error
Now, you could manually define Prop
, but if you'd like the compiler to compute Prop
in terms of CleanPropTypes
, you can mostly do so by treating each of those
primitive wrappers as a function that produces a value of the relevant primitive type. For example:
type Prop2 = CleanPropTypes extends infer C ?
C extends (...args: any) => infer R ?
{ type: C, default?: R }
: never : never;
Here I'm using conditional type inference twice. The first time, CleanPropTypes extends infer C ? ... : never
basically just copies the CleanPropTypes
specific type into the new type parameter C
. Then, when we write C extends (...args: any) => infer R ? ... : never
, we are getting the return type R
of the function in C
. The reason we do the copying first is so that C extends ... ? ... : ...
becomes a distributive conditional type, breaking the CleanPropTypes
union into individual elements, and evaluating { type: C, default?: R }
for each such element, and then putting them back together in a union.
Anyway, this is almost what you want:
/* type Prop2 = {
type: ArrayConstructor;
default?: unknown[] | undefined;
} | {
type: ObjectConstructor;
default?: any;
} | {
type: FunctionConstructor;
default?: Function | undefined;
} | {
type: BooleanConstructor;
default?: boolean | undefined;
} | {
type: StringConstructor;
default?: string | undefined;
} */
Everything is correct except for the ObjectContructor
element. Here, the default
property is of the any
type which allows anything, except for the more correct object
type which only allows non-primitives. I assume the call signature for Object
predates the introduction of object
. See ms/TS#13741 for some discussion about this.
Anyway, since that one isn't working for us, we can do just that one manually, and then produce the rest from Exclude<CleanPropTypes, ObjectConstructor>
, where we use the Exclude<T, U>
utility type to remove ObjectConstructor
from the union:
type Prop3 = (Exclude<CleanPropTypes, typeof Object> extends infer C ?
C extends (...args: any) => infer I ?
{ type: C, default?: I } : never : never
) | { type: ObjectConstructor, default?: object }
And that produces the same type as Prop
above.
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With