typescript - 如何从现有类型定义类型但使用可选键(不使用 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
}
解决方案
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-,请参阅下面的原始答案。
原答案:
您已经发现 TypeScript 并不能很好地区分一个缺少的属性与一个存在但其值为undefined
. 从仅读取其中一个属性的角度来看,确实没有区别。foo.bar === undefined
将true
在任何一种情况下。但当然,在某些情况下存在明显差异,例如当您传播对象时,甚至是像"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
,除了a
、b
和c
属性现在是“可选的”。您必须传入 alocalId
类型number
(Omit<Entity, "a" | "b" | "c">
等价于{localId: number}
)。您可以完全省略a
,b
和c
属性(您遗漏的任何键都不在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
这一切都是正确的。编译器抱怨undefined
for a
,但不抱怨 for b
。并且编译器识别出它fullEntity
实际上是一个Entity
,因为它看到它definedAC
并且undefinedB
一起具有 的所有属性Entity
。
正如我所说,这是microsoft/TypeScript#13195的解决方法。它比我们想要的更复杂,因为它难以表达 TypeScript 不容易做到的东西:两者之间存在差异undefined
和缺失。不幸的是,如果您想尝试这样做,辅助函数和泛型是必不可少的。
解决这个问题的另一种方法是接受 TypeScript 无法区分的能力,并针对undefined
. 我不打算在这里详细介绍,因为这已经是一个很长的答案了。但是,作为一个草图:您可以编写一个函数stripUndefined
,它遍历对象的属性并删除任何值为undefined
. 如果您传播该函数的输出,您就不必再担心流氓undefined
实际上会覆盖另一个属性。
推荐阅读
- wordpress - 后台根据SKU排序订单
- c# - C#/UWP 中的屏幕截图
- javascript - 在 react 中更改 react-calendar 的日期格式
- spring-data-elasticsearch - spring data Elasticsearch 动态映射
- php - 在 AWS 中设置 Web 服务器数据库 - 无法理解指令的 dbinfo.inc 文件部分
- reactjs - 从另一个文件加载 React 组件
- keras - 当我添加 step_per_epoch 且 epoch=1 时,训练 ETA 为 5 小时
- flutter - 由于版本失败,获取包无法正常工作
- javascript - Laravel 7:如何在依赖下拉列表中获取名称而不是 id?
- javascript - reCaptcha v3 不执行