如何使用映射类型获取对象类型的可选部分?

4
例如,我有这个类型。
type Foo = {
    foo?: number
    bar: string
    obj?: {
        qwe?: number
        asd: string
    }
}

我希望能够创建一个类型

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

我尝试过这个

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 = {
}

但是它不起作用,我不确定原因。

我可以建议您在问题中添加更多信息吗?比如语言、语言版本、编辑器等等,以便更好地理解。 - Lorenzo
1个回答

7

问题的第一部分涉及到你的OptionalKeys类型。关系是相反的,如果你有一个联合类型,那么联合类型是成员类型的超类型,而不是相反的。例如:

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

因此,在我们的案例中,OptionalKeys 将是:

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

我们还需要从键中排除undefined,因为属性是可选的,所以它会在其中。
问题的第二部分是如何构建递归类型别名,以便在所有情况下都能正常工作。为此,我们可以参考在此处找到的DeepReadOnly示例。
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 }
    ]
}

编辑

正如@jcalz所指出的,可以使用非严格空值检查版本来实现此类型:

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

这个功能的工作原理是通过测试类型中从 P 属性选择的结果是否可分配给 {},如果是,则意味着属性 P 是可选的,因为如果它是必需的,则生成的 Pick 将不可分配给 {}
这个版本实际上会更好地挑选出仅可选属性以及类型为 baseType | undefined 的必需属性,这可能是一个优点或缺点,具体取决于您的用例。

你可以像这样获取类型的可选键,无论是否使用strictNullChecks: type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T]。这会检查“缺失键”部分而不是“可能未定义值”的部分。我会自己回答,但我不想干扰你的工作。 - jcalz
@jcalz 我改口了,早上回答这个问题时我在想是否真的需要使用 strictNullChecks,但是因为当时没有电脑测试我的想法,所以没有机会去尝试。在严格的空值检查下,这肯定更加直接明了。感谢你提供非严格空值检查选项,我会将其添加到答案中 :) - Titian Cernicova-Dragomir
即使进行了严格的空值检查,上述代码仍然可以区分{a?: string}{a: string | undefined},如果有必要的话...(尽管TypeScript在一般情况下并不擅长区分它们) - jcalz
@jcalz 是的,这是真的,我也加了一个解释。然而,对于一些人来说,似乎仍然很惊讶,即类型为 baseType|undefined 的属性仍然是必需的,我敢肯定我在 GitHub 上看到过一个关于这个问题的票据.. - Titian Cernicova-Dragomir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接