microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.77k stars 12.46k forks source link

Change property modifier in mapped type based on condition #32562

Open pleerock opened 5 years ago

pleerock commented 5 years ago

As I know there is no way to change property modifier of mapped types based on conditional types. Here is my use case:

export class MakeItRequired<T extends ModelValue<any>> {
    target: "MakeItRequired" = "MakeItRequired"
    constructor(public option: T) {    
    }
}

export type ModelValue<T> = 
    T extends StringConstructor ? string : 
    T extends NumberConstructor ? number : 
    T extends BooleanConstructor ? boolean :
    unknown

export type ModelFromSchema<T> = {
    [P in keyof T]?: 
        T[P] extends MakeItRequired<infer U> ? ModelValue<U> :
        ModelValue<T[P]>
}

export const schema = {
    id: new MakeItRequired(Number),
    firstName: String,
    lastName: String
}

export const type: ModelFromSchema<typeof schema> = {}

My goal is to have id non optional, while having others optional. Additional screenshoot from vscode:

Screenshot 2019-07-25 22 59 49

Can we have this feature?

pleerock commented 5 years ago

~Okay, I think I found the way it can be implemented right now:~

export type ModelFromSchema<T> = {
    [P in keyof T]+?: 
        T[P] extends MakeItRequired<infer U> ? unknown :
        ModelValue<T[P]>
} & {
    [P in keyof T]-?: 
        T[P] extends MakeItRequired<infer U> ? ModelValue<U> :
        unknown
}

~Looks like unknown saves us here.~

~Issue can be closed, but I would like team to pay attention on the scalability of the current approach.~

EDIT 1: No, I was too happy about it. It doesn't work.

EDIT 2: Okay, found a resolution:

type MakeItRequiredKeyNames<T> = { [K in keyof T]: T[K] extends MakeItRequired<infer U> ? K : never }[keyof T];
type MakeItRequiredKeys<T> = Pick<T, MakeItRequiredKeyNames<T>>;

type NonMakeItRequiredKeyNames<T> = { [K in keyof T]: T[K] extends MakeItRequired<infer U> ? never : K }[keyof T];
type NonMakeItRequiredKeys<T> = Pick<T, NonMakeItRequiredKeyNames<T>>;

export type ModelFromSchema<T> = {
    [P in keyof NonMakeItRequiredKeys<T>]?: 
        T[P] extends MakeItRequired<infer U> ? never :
        ModelValue<T[P]>
} & {
    [P in keyof MakeItRequiredKeys<T>]: 
        T[P] extends MakeItRequired<infer U> ? ModelValue<U> :
        never
}
AnyhowStep commented 5 years ago

Just note that your types might look a little "ugly" in the tooltip. It'll still work just fine, though.

If you want to make it look a little better (maybe for debugging or something),

Identity<T> = T;
Merge<T> = (
  T extends any ?
  Identity<{ [k in keyof T] : T[k] }> :
  never
);

Then,

type Blah = Merge<ModelFromSchema<T>>;

When you hover over Blah, you should see the two objects merged into one


I'm on mobile and can't test it but I think the above works. I'll check again when I get home

jcalz commented 5 years ago

Possibly relevant comment from another somewhat related issue (#31581, about detecting such modifiers)

I'd be interested in more streamlined manipulation of modifiers in mapped types. Detecting `readonly` and optional properties is indeed a bunch of hoop jumping, and then if you want to selectively *alter* the modifiers, you need to split the mapping into pieces and intersect them: ```ts type SelectivePartial = Partial> & Required>> extends infer U ? { [P in keyof U]: U[P] } : never; type Foo = SelectivePartial<{ a: string, b: number, c?: boolean }, 'b'> // type Foo = { b?: number | undefined; a: string; c: boolean; } ``` It would be a lot nicer to essentially read and selectively write modifiers inside the mapping directly. (Note the word "selectively"; the [current support](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#improved-control-over-mapped-type-modifiers) for altering modifiers is all-or-nothing ).
codinsonn commented 3 years ago

Just note that your types might look a little "ugly" in the tooltip. It'll still work just fine, though.

If you want to make it look a little better (maybe for debugging or something),

Identity<T> = T;
Merge<T> = (
  T extends any ?
  Identity<{ [k in keyof T] : T[k] }> :
  never
);

Then,

type Blah = Merge<ModelFromSchema<T>>;

When you hover over Blah, you should see the two objects merged into one

I'm on mobile and can't test it but I think the above works. I'll check again when I get home

This works, but also seems to turn types like Date into { toString: () => string, ... } Would there be a way to prevent this?