简体   繁体   中英

How to get an optional part of object type with mapped type?

For example I have this type

type Foo = {
    foo?: number
    bar: string
    obj?: {
        qwe?: number
        asd: string
    }
}

and I want to have a type

type Foo2 = {
    foo: number
    obj: {
        qwe: number
    }
}

I have tried this

type OptionalKeys<T> = {[P in keyof T]: T[P] extends undefined ? P : never }[keyof T]

type PickOptionalProperties<T> = {
    [P in OptionalKeys<T>]-?: PickOptionalProperties<T[P]>
};
type Foo2 = PickOptionalProperties<Foo>  
const o: Foo2 = {
}

But it doesn't work and I'm not sure why

The first part of the problem is in your OptionalKeys type. The relation is inversed, if you have a union, the union is the super type of a member, not the other way around . For example:

type N = number | undefined extends number ? "Y": "N" // will be "N"
type Y = number  extends number | undefined ? "Y": "N" // will be "Y"

So in our case, OptionalKeys will be:

type OptionalKeys<T> = {[P in keyof T]-: undefined extends T[P]? P : never }[keyof T]

We also need to exclude undefined from the keys, since it will be in there because of the optionality of the properties.

The second part of the problem is how to construct a recursive type alias that will work correctly in all situations. For this we can turn to the DeepReadOnly example found here

type Foo = {
    foo?: number
    fooArray?: number[]
    bar: string
    obj?: {
        qwe?: number
        asd: string
    }
    objArray?: Array<{
        qwe?: number
        asd: string
    }>
}

type OptionalKeys<T> = Exclude<{ [P in keyof T]: undefined extends T[P] ? P : never }[keyof T], undefined>

type primitive = string | number | boolean | undefined | null
type PickOptionalProperties<T> =
    T extends primitive ? T :
    T extends Array<infer U> ? PickOptionalPropertiesArray<U> :
    PickOptionalPropertiesObject<T>

interface PickOptionalPropertiesArray<T> extends ReadonlyArray<PickOptionalProperties<T>> { }

type PickOptionalPropertiesObject<T> = {
    readonly [P in OptionalKeys<T>]: PickOptionalProperties<Exclude<T[P], undefined>>
}


type Foo24 = PickOptionalProperties<Foo>
const o: Foo24 = {
    foo: 0,
    fooArray: [1, 2],
    obj: {
        qwe: 1,
    },
    objArray: [
        { qwe: 1 }
    ]
}

Edit

As @jcalz pointed out a non strict null check version of this type is possible using:

 type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T]

The way this works is by testing whether a pick of the P property from he type, is assignable to {} if so it means the property P is optional as if it were required the resulting Pick would not be assignable to {} .

This version will actually do a better job of picking out just the optional properties, and required properties of type baseType | undefined baseType | undefined which my be a plus or a minus depending on your use case.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM