5

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 ?

11
  • 1
    Is this what you're looking for? If so, I can write up an answer. If not, please edit the question to demonstrate unsatisfied use cases. Commented Nov 4, 2021 at 18:35
  • Nuts! Its works. I don`t know to hide long link. there is only one example let b : Prop = { type:Object, default:'STR' } // must be err But otherwise everything works great. thx a lot. I will parse your code Commented Nov 4, 2021 at 19:23
  • 1
    You can use tsplay.dev to shorten links to the TypeScript Playground, but I'm not sure I understand what you're saying. Maybe you added the comment prematurely? Commented Nov 4, 2021 at 19:25
  • only counter example Commented Nov 4, 2021 at 19:27
  • 1
    I guess you can't infer object from the Object constructor, so you need to special case it like this maybe. Of course you could just do it manually like this instead. If one of those works for you let me know and I can write it up. Commented Nov 4, 2021 at 19:32

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.