【问题标题】:TypeScript Partial<T> type without undefined没有未定义的 TypeScript Partial<T> 类型
【发布时间】:2019-02-02 03:51:46
【问题描述】:

如何创建一个 kinda-Partial&lt;T&gt; 类型,不允许 undefined 值?

这是一个例子:

interface MyType {
  foo: string
  bar?: number
}

const merge = (value1: MyType, value2: KindaPartial<MyType>): MyType => {
  return {...value1, ...value2};
}



const value = {
  foo: 'foo',
  bar: 42
}

merge(value, {});                          // should work
merge(value, { foo: 'bar' });              // should work
merge(value, { bar: undefined });          // should work
merge(value, { bar: 666 });                // should work
merge(value, { foo: '', bar: undefined }); // should work
merge(value, { foo: '', bar: 666 });       // should work

// now the problematic case:
merge(value, { foo: undefined }); // this should throw an error
                                  // because MyType["foo"] is of type string

我正在寻找的类型应该:

  • 只接受存在于泛型类型上的键(就像普通的Partial&lt;T&gt;
  • 接受泛型键的子集
  • 如果泛型类型不接受 undefined 作为该键,则不接受 undefined

这可能吗?


编辑:我还在 TypeScript 存储库中创建了一个问题,因为这很奇怪,应该在某个时候抛出错误:https://github.com/Microsoft/TypeScript/issues/29701

【问题讨论】:

  • 请记住,这样做可能会使您的代码更难以编程方式使用;你会禁止例如update(value: MyType, new_foo_val?: string) { return merge(value, { foo: new_foo_val }).

标签: typescript generics partial


【解决方案1】:

TS 4.4 更新:

TS4.4 将具有 an --exactOptionalPropertyTypes compiler flag 以直接使用 Partial 为您提供您正在寻找的行为,只要您有意在您希望允许的位置添加 undefined

interface MyType {
  foo: string
  bar?: number | undefined // <-- you want this
}

const merge = (value1: MyType, value2: Partial<MyType>): MyType => {
  return { ...value1, ...value2 };
}


const value = {
  foo: 'foo',
  bar: 42
}

merge(value, {});                          // okay
merge(value, { foo: 'bar' });              // okay
merge(value, { bar: undefined });          // okay
merge(value, { bar: 666 });                // okay
merge(value, { foo: '', bar: undefined }); // okay
merge(value, { foo: '', bar: 666 });       // okay

// now the problematic case:
merge(value, { foo: undefined }); // error!
// ----------> ~~~
// Type 'undefined' is not assignable to type 'string'

Playground link to code --exactOptionalPropertyTypes;由于某种原因,网址已损坏

——--

PRE-TS4.4 答案:

TypeScript 无法正确区分 缺少 的对象属性(和函数参数)与 存在但 @987654332 的对象属性(和函数参数)是一个已知限制(参见 microsoft/TypeScript#13195) @Partial&lt;T&gt; 允许 undefined 属性的事实是其结果。正确的做法是等到此问题得到解决(如果您在 GitHub 中访问该问题并给它一个 ? 或带有令人信服的用例的评论,这可能会变得更有可能)。

如果您不想等待,您可以使用以下 hacky 方式来获得类似这种行为:

type VerifyKindaPartial<T, KP> = 
  Partial<T> & {[K in keyof KP]-?: K extends keyof T ? T[K] : never}; 

const merge = <KP>(value1: MyType, value2: KP & VerifyKindaPartial<MyType, KP>): MyType => {
  return { ...value1, ...value2 };
}

所以你不能直接写KindaPartial&lt;T&gt;。但是您可以编写一个类型 VerifyKindaPartial&lt;T, KP&gt;,它接受一个类型 T 和一个 candidate 类型 KP,您想检查您想要的 KindaPartial&lt;T&gt;。如果候选匹配,则返回匹配KP 的内容。否则它会返回不存在的东西。

然后,您将merge() 设为generic 函数,该函数从传递给value2 的值的类型推断KP。如果KP &amp; VerifyKindaPartial&lt;MyType, KP&gt; 匹配KP(意味着KP 匹配KindaPartial&lt;MyType&gt;),则代码将编译。否则,如果KP &amp; VerifyKindaPartial&lt;MyType, KP&gt; not 匹配KP(意味着KP 不匹配KindaPartial&lt;MyType&gt;),则会出现错误。 (不过,错误可能不是很直观)。

让我们看看:

merge(value, {});                          // works
merge(value, { foo: 'bar' });              // works
merge(value, { bar: undefined });          // works
merge(value, { bar: 666 });                // works
merge(value, { foo: '', bar: undefined }); // works
merge(value, { foo: '', bar: 666 });       // works

merge(value, { foo: undefined }); // error!
//             ~~~ <-- undefined is not assignable to never
//  the expected type comes from property 'foo', 

这有你想要的行为......虽然你得到的错误有点奇怪(理想情况下它会说undefined不能分配给string,但问题是编译器知道传入的类型是undefined,它希望类型是string,所以编译器将这些相交到undefined &amp; string,即never。哦,好吧。

无论如何,这里可能有一些警告;泛型函数在直接调用时运行良好,但由于 TypeScript 对更高种类的类型的支持不是很好,所以它们不能很好地组合。我不知道这是否真的适用于您的用例,但这是我能用目前的语言做的最好的事情。

Playground link to code

【讨论】:

  • 感谢您的解释!我对这个问题竖起了大拇指。自从 TypeScript 和 Partial 出现以来,我一直在使用它,但这是我第一次偶然发现这个问题。
  • 因为它需要所有的属性,而且根本不是局部的?
  • 不,请参阅documentation for mapped types{[P in K]: X}K 的每个联合成员都有一个属性。
  • 您的KindaMyTypeMyType 的类型相同。您可以(不安全地)使用类型断言(您称之为“强制转换”)将{bar: 666} 缩小到MyType 这一事实与接受部分MyType 的函数不同。我很惊讶你没有在你的 cmets 中提到类型断言,因为这是你解决方法的关键。
【解决方案2】:

在这种情况下,Pick 应该可以工作。

interface MyType {
  foo: string
  bar?: number
}

const merge = <K extends keyof MyType>(value1: MyType, value2: Pick<MyType, K>): MyType => {
  return {...value1, ...value2};
}

merge(value, {});                          // ok
merge(value, { foo: 'bar' });              // ok
merge(value, { bar: undefined });          // ok
merge(value, { bar: 666 });                // ok
merge(value, { foo: '', bar: undefined }); // ok
merge(value, { foo: '', bar: 666 });       // ok

merge(value, { foo: undefined });          // ng

Playground link

【讨论】:

    猜你喜欢
    • 2019-07-25
    • 1970-01-01
    • 1970-01-01
    • 2020-03-10
    • 1970-01-01
    • 2019-01-07
    • 2021-05-05
    • 2021-10-04
    • 1970-01-01
    相关资源
    最近更新 更多