简体   繁体   English

如何从现有类型定义类型但使用可选键(不使用 Partial)?

[英]How to define a type from an existing type but with optional keys (not with Partial)?

I'm doing some state merging in React/Redux along the way of:我正在做一些 state 合并到 React/Redux 中:

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

My issue is that I want to define the type of action.payload so that it has the same exact type as a member of state.activeData.entities but with all props being optional (except localId) so that I can merge the keys I want to change and leave the rest unchanged.我的问题是我想定义 action.payload 的类型,使其与 state.activeData.entities 的成员具有相同的类型,但所有道具都是可选的(localId 除外),以便我可以合并我想要的键更改并保持 rest 不变。

Let's call this base type "Entity" and let's assume the following structure:让我们将此基本类型称为“实体”,并假设以下结构:

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

What I want to get is:我想要得到的是:

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

I tried the following:我尝试了以下方法:

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

The reason this doesn't work for me is that Partial allows the keys to have an undefined value (which I don't want to allow) so the resulting type is:这对我不起作用的原因是 Partial 允许键具有未定义的值(我不想允许),因此结果类型是:

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

I could, of course, manually write this out but I need to do this for every entity and probably again in the future and it's not very DRY... Is there some operator like Partial that does what I want?当然,我可以手动将其写出来,但我需要为每个实体执行此操作,并且将来可能会再次执行此操作,而且它不是很干燥……是否有像 Partial 这样的操作员可以满足我的要求?

EDIT: Turns out "c?: Date | undefined" and "c?: Date" are equivalent but what I really want is for typeScript to allow not setting the key but throw an error if it's set to undefined because undefined still overwrites my existing values when I spread an object like this:编辑:结果“c?:Date | undefined”和“c?:Date”是等价的,但我真正想要的是 typeScript 允许不设置密钥,但如果它设置为 undefined 则会抛出错误,因为 undefined 仍然会覆盖我现有的当我像这样传播 object 时的值:

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

c is now: c 现在是:

{
    prop_a: undefined,
    prop_b: 3
}

You've discovered that TypeScript doesn't do a perfect job of telling the difference between a property which is missing from one which is present but whose value is undefined .您已经发现 TypeScript 并不能完美地分辨出缺少的属性与存在但其值为undefined的属性之间的区别。 From the point of view of just reading one of these properties, there really is no difference.从仅读取其中一个属性的角度来看,确实没有区别。 foo.bar === undefined will be true in either case. foo.bar === undefined在任何一种情况下都将为true But of course, there are situations where there is a noticeable difference, such as when you spread an object, or even a simple test like "bar" in foo .但当然,在某些情况下存在明显差异,例如当您传播 object 时,甚至是"bar" in foo类的简单测试。 This mismatch between the TypeScript type system and the behavior of Javascript is a longstanding open issue; TypeScript 类型系统与 Javascript 的行为之间的这种不匹配是一个长期存在的问题; see microsoft/TypeScript#13195 for a lengthy discussion.请参阅microsoft/TypeScript#13195进行冗长的讨论。 If you want to see this addressed, you could give it a, but it's not clear to me if or when this will ever happen.如果你想看到这个问题,你可以给它一个,但我不清楚这是否会发生或何时发生。

For the time being, there are workarounds.目前,有一些解决方法。


The workaround I recommend here is to avoid the built-in optional property functionality, and instead make a generic helper function that, for the properties that would be optional, accepts a value without any such property, or a value with a non- undefined property.我在这里推荐的解决方法是避免内置的可选属性功能,而是创建一个通用帮助器function ,对于可选的属性,它接受没有任何此类属性的值,或具有非undefined属性的值. It looks something like this:它看起来像这样:

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;

The strictPartial() function is a curried function. strictPartial() function 是一个咖喱function。 It takes a two type parameters: T , the object type in question (with no optional properties), and K , the union of keys you'd like to make "optional" in the way I described above.它有两个类型参数: T ,有问题的 object 类型(没有可选属性),以及K ,你想按照我上面描述的方式使“可选”的键的联合 It then outputs a new function which only accepts objects of such a "strict partial" type.然后它输出一个新的 function ,它只接受这种“严格部分”类型的对象。 The return value of that function is the same as its input, so the compiler keeps track of which properties are actually present.function 的返回值与其输入相同,因此编译器会跟踪实际存在的属性。

If you inspect the constraint on the U type parameter, you'll see that it is the intersection of Omit<T, K> , a type with all the properties of T not mentioned in K , and a mapped type where every property from U is either matched with its property from T , or is never .如果您检查对U类型参数的约束,您会发现它是Omit<T, K>交集,一个类型具有T的所有未在K中提及的属性,以及一个映射类型,其中每个属性都来自U要么与T中的属性匹配,要么never匹配。 We will explore how that works below.我们将在下面探讨它是如何工作的。


Here it is for the particular Entity type:这是针对特定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 */

So strictPartialEntity() is a function that takes an object of type U .所以strictPartialEntity()是一个 function ,它采用U类型的 object 。 This U type is like Entity , except that a , b , and c properties are now "optional".这种U类型类似于Entity ,除了abc属性现在是“可选的”。 You must pass in a localId of type number ( Omit<Entity, "a" | "b" | "c"> is equivalent to {localId: number} ).您必须传入number类型的localIdOmit<Entity, "a" | "b" | "c">等价于{localId: number} )。 You are allowed to leave out a , b , and c properties entirely (any key you leave out is not in keyof U , so [P in keyof U] will ignore it), or specify them as the right types (any key P you put in must match keyof Entity , and its value must match the same type, Entity[P] ... or else it must match never which is impossible).您可以完全省略abc属性(您遗漏的任何键都不在keyof U中,因此[P in keyof U]将忽略它),或者将它们指定为正确的类型(任何键P你put in 必须匹配keyof Entity ,并且它的值必须匹配相同的类型, Entity[P] ... 否则它必须never匹配,这是不可能的)。

Note that only b will accept undefined , and that's because b is explicitly typed to allow it in Entity .请注意,只有b将接受undefined ,这是因为b被显式键入以允许它在Entity中。

Let's make sure it behaves that way:让我们确保它的行为方式是这样的:

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

That all behaves correctly.这一切都是正确的。 The compiler complains about undefined for a , but not for b .编译器抱怨undefined a ,但不抱怨b And the compiler recognizes that fullEntity is in fact an Entity , because it sees that definedAC and undefinedB together have all the properties of Entity .并且编译器认识到fullEntity实际上是一个Entity ,因为它看到definedACundefinedB一起具有Entity的所有属性。


As I said, this is a workaround to microsoft/TypeScript#13195 .正如我所说,这是microsoft/TypeScript#13195的解决方法。 It's more complicated than we'd like because it struggles to express something that TypeScript doesn't make easy: that there is a difference between undefined and missing.它比我们想要的更复杂,因为它难以表达 TypeScript 不容易做到的东西: undefined和缺失之间存在差异。 The helper functions and generics are an unfortunate necessity if you want to try to do this.不幸的是,如果您想尝试这样做,辅助函数和 generics 是必不可少的。

The other way to approach this is to embrace TypeScript's inability to tell the difference, and defensively code against undefined .解决这个问题的另一种方法是接受 TypeScript 无法区分的能力,并针对undefined进行防御性编码。 I'm not going to go into details here, since this is already quite a long answer.我不会在这里详细介绍 go,因为这已经是一个很长的答案了。 But, as a sketch: you could write a function like stripUndefined which iterates over an object's properties and removes any properties whose value is undefined .但是,作为一个草图:你可以写一个 function 像stripUndefined迭代对象的属性并删除任何值为undefined的属性。 If you spread the output of that function, you don't have to worry anymore about a rogue undefined actually overwriting another property.如果您传播该 functionoutput,您不必再担心流氓undefined实际上会覆盖另一个属性。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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