Skip to content

Change property modifier in mapped type based on condition #32562

Open

Description

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?

Activity

  1. pleerock commented on Jul 25, 2019

    @pleerock
    Author

    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
    }
  2. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    Contributor

    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

  3. jcalz commented on Jul 25, 2019

    @jcalz
    Contributor

    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:
    type SelectivePartial<T, K extends keyof T> =
      Partial<Pick<T, K>> & Required<Pick<T, Exclude<keyof T, K>>> 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 for altering modifiers is all-or-nothing ).

  4. codinsonn commented on Jul 25, 2021

    @codinsonn

    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?

  5. colinhacks commented on Oct 9, 2026

    @colinhacks
    Contributor

    Zod's object types need this. Whether a key is optional depends on the schema at that key, so the output type is two filtered mapped types intersected. That shape has two problems a single mapped type doesn't.

    interface Schema<O = unknown> { output: O; optional?: boolean }
    interface Optional<O> extends Schema<O | undefined> { optional: true }
    type Shape = Record<string, Schema>;
    
    type Infer<T extends Shape> = {
      [K in keyof T as T[K] extends { optional: true } ? never : K]: T[K]["output"];
    } & {
      [K in keyof T as T[K] extends { optional: true } ? K : never]?: T[K]["output"];
    };
    
    function get<T extends Shape, K extends keyof T>(data: Infer<T>, key: K) {
      return data[key]; // error 2536
    }
    
    type Prettify<T> = { [K in keyof T]: T[K] } & {};
    declare const value: Prettify<Infer<{ b: Schema<string>; a: Optional<number>; c: Schema<boolean> }>>;
    // { b: string; c: boolean; a?: number | undefined }

    Generic code can't index the output with keyof T, and every optional key moves to the end, in hovers and in .d.ts output. With { [K in keyof T]: T[K]["output"] } both go away, but there's no way to make some of those keys optional. Putting an all-optional copy of the shape first keeps the declared order, but it breaks self-referential schemas with 2615 circularity errors.

    Still the case on 7.1.0-dev.20261006.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureDomain: Mapped TypesThe issue relates to mapped typesSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions