首页 > 解决方案 > 如何从现有类型定义类型但使用可选键(不使用 Partial)?

问题描述

我正在 React/Redux 中进行一些状态合并:

const newStateSlice={
    ...state.activeData.entities[action.payload.localId],
    ...action.payload,
    isModified: true
}

我的问题是我想定义 action.payload 的类型,使其与 state.activeData.entities 的成员具有相同的类型,但所有道具都是可选的(localId 除外),这样我就可以合并我想要的键改变并保持其余不变。

让我们将此基本类型称为“实体”,并假设以下结构:

interface Entity{
    localId: number
    a: string
    b: number | undefined
    c?: Date
}

我想要得到的是:

interface PartialEntity{
    localId: number
    a?: string
    b?: number | undefined
    c?: Date
}

我尝试了以下方法:

type PartialEntity = Partial<NewFeeds.State.ActiveFeed> & { localId: number }

这对我不起作用的原因是 Partial 允许键具有未定义的值(我不想允许),因此结果类型是:

type PartialEntity{
    localId: number,
    a?: string | undefined,
    b?: number | undefined,
    c?: Date | undefined
}

当然,我可以手动将其写出来,但我需要为每个实体执行此操作,并且将来可能会再次执行此操作,而且它不是很干燥……是否有像 Partial 这样的操作员可以满足我的要求?

编辑:原来 "c?: Date | undefined" 和 "c?: Date" 是等价的,但我真正想要的是 typeScript 允许不设置密钥,但如果它设置为 undefined 则会抛出错误,因为 undefined 仍然会覆盖我现有的当我传播这样的对象时的值:

const a = {prop_a: 1, prop_b: 2}
const b = {prop_a: undefined, prop_b: 3}
const c = {...a, ...b};

c 现在是:

{
    prop_a: undefined,
    prop_b: 3
}

标签: typescript

解决方案


TS4.4+ 更新:

现在有一个--exactOptionalPropertyTypes编译器选项将阻止您写入 undefined可选属性(除非该值undefined像您的 一样显式接受b)。此编译器选项不是编译器功能套件的一部分--strict,因此它不像选项那样真正被视为“标准” strict。如果启用它,它可能会导致代码的其他部分出错,这可能是可接受的,也可能是不可接受的。如果您确实想启用它,那么它将解决您的问题:

type Partialize<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>
type PartialEntity = Partialize<Entity, "a" | "b" | "c">;

const noLocalId: PartialEntity = {}; // error
// Property 'localId' is missing in type

const definedAC: PartialEntity = { localId: 1, a: "hello", c: new Date() }; // okay

const undefinedA: PartialEntity = { localId: 1, a: undefined }; // error!
//     Types of property 'a' are incompatible.

const undefinedB: PartialEntity = { localId: 2, b: undefined }; // okay

const unexpectedD: PartialEntity = { localId: 4, d: "oops" } // error!
// Object literal may only specify known properties

如果您不想启用它或正在使用 TS4.3-,请参阅下面的原始答案。

Playground 代码链接


原答案:

您已经发现 TypeScript 并不能很好地区分一个缺少的属性与一个存在但其值为undefined. 从仅读取其中一个属性的角度来看,确实没有区别。foo.bar === undefinedtrue在任何一种情况下。但当然,在某些情况下存在明显差异,例如当您传播对象时,甚至是像"bar" in foo. TypeScript 类型系统和 Javascript 行为之间的这种不匹配是一个长期悬而未决的问题。见微软/TypeScript#13195进行长时间的讨论。如果你想看到这个问题,你可以给它一个,但我不清楚这是否会发生或何时发生。

目前,有一些解决方法。


我在这里推荐的解决方法是避免内置的可选属性功能,而是创建一个通用辅助函数,对于可选的属性,它接受一个没有任何此类属性的值,或者一个具有非undefined属性的值。它看起来像这样:

const strictPartial = <T, K extends keyof T>() =>
    <U extends {
        [P in keyof U]-?: P extends keyof T ? T[P] : never
    } & Omit<T, K>>(u: U) => u;

strictPartial()函数是一个柯里化函数。它有两个类型参数:T有问题的对象类型(没有可选属性),以及您希望按照我上面描述的方式“可选”的键K联合。然后它输出一个新函数,它只接受这种“严格部分”类型的对象。函数的返回值与其输入相同,因此编译器会跟踪实际存在的属性。

如果您检查 type 参数的约束U您会看到它是 的交集Omit<T, K>一个具有 中T未提及的所有属性的类型K,以及一个映射类型,其中每个属性 fromU都与其属性 from 匹配T,或者是never. 我们将在下面探讨它是如何工作的。


这是针对特定Entity类型的:

interface Entity {
    localId: number
    a: string
    b: number | undefined
    c: Date // <-- this is no longer optional, as mentioned above
}

const strictPartialEntity = strictPartial<Entity, "a" | "b" | "c">();
/* const strictPartialEntity: <U extends 
  { [P in keyof U]-?: P extends keyof Entity ? Entity[P] : never; } & 
  Omit<Entity, "a" | "b" | "c">>(u: U) => U */

strictPartialEntity()接受类型对象的函数也是如此U。这种U类型类似于Entity,除了abc属性现在是“可选的”。您必须传入 alocalId类型numberOmit<Entity, "a" | "b" | "c">等价于{localId: number})。您可以完全省略a,bc属性(您遗漏的任何键都不在keyof U,因此[P in keyof U]将忽略它​​),或将它们指定为正确的类型(您输入的任何键P都必须匹配keyof Entity,并且它的值必须匹配相同type, Entity[P]... 否则它必须匹配never这是不可能的)。

请注意, onlyb将接受undefined,这是因为b明确输入允许它进入Entity

让我们确保它的行为方式是这样的:

const noLocalId = strictPartialEntity({}); // error! 
// Property 'localId' is missing ---> ~~

const definedAC = strictPartialEntity(
    { localId: 1, a: "hello", c: new Date() }) // okay

const undefinedA = strictPartialEntity(
    { localId: 1, a: undefined }) // error! 
// -------------> ~
// Type 'undefined' is not assignable to type 'string'

const undefinedB = strictPartialEntity(
    { localId: 2, b: undefined }) // okay, 
// b explicitly allows undefined

const unexpectedD = strictPartialEntity(
    { localId: 4, d: "oops" }) // error!
// -------------> ~
//  Type 'string' is not assignable to type 'never'

const fullEntity: Entity = {
    ...definedAC,
    ...undefinedB
} // okay

这一切都是正确的。编译器抱怨undefinedfor a,但不抱怨 for b。并且编译器识别出它fullEntity实际上是一个Entity,因为它看到它definedAC并且undefinedB一起具有 的所有属性Entity


正如我所说,这是microsoft/TypeScript#13195的解决方法。它比我们想要的更复杂,因为它难以表达 TypeScript 不容易做到的东西:两者之间存在差异undefined和缺失。不幸的是,如果您想尝试这样做,辅助函数和泛型是必不可少的。

解决这个问题的另一种方法是接受 TypeScript 无法区分的能力,并针对undefined. 我不打算在这里详细介绍,因为这已经是一个很长的答案了。但是,作为一个草图:您可以编写一个函数stripUndefined,它遍历对象的属性并删除任何值为undefined. 如果您传播该函数的输出您就不必再担心流氓undefined实际上会覆盖另一个属性。

Playground 代码链接


推荐阅读