Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a type in typescript in which the type of one value will depend on the type of another value in the object

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 ?

like image 504
Artem Skibin Avatar asked Nov 06 '22 00:11

Artem Skibin


1 Answers

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

like image 58
jcalz Avatar answered Nov 11 '22 05:11

jcalz