要求类型不为空

huangapple go评论68阅读模式
英文:

How to require a type to be not empty

问题

假设我有一种类型,其中所有属性都是可选的:

type T = {
x?: number;
y?: string;
z?: number;
...
}


有没有一种方法可以构造一种类型,允许所有类型T的值,但要求至少有一个属性?例如:

let a: NotEmpty<T> = { z: 1 }; // 正确
a = {}; // 错误

当属性数量较多时,一种方法是使用至少要求一个属性的所有组合的联合,但当属性数量较大时,这种方法变得有点繁琐。是否有一种更通用的方法?
英文:

Suppose I have a type where all properties are optional:

type T = {
  x?: number;
  y?: string;
  z?: number;
  ...
}

Is there a way to construct a type that will allow all values of type T but requires at least one property? E.g.:

let a: NotEmpty&lt;T&gt; = { z: 1 }; // ok
a = {}; // error

Of course one way to do this would be a union of all combinations with at least one required property, but this becomes kind of awkward when the number of properties is large. Is there a more general way?

答案1

得分: 3

以下是翻译好的部分:

"union",你所谈论的“至少有一个必需属性的所有组合”的联合类型,写起来并不太难,尤其是因为你可以编写NotEmpty<T>实用类型来自动计算它。而且该类型每个属性键只有一个联合成员,因此随着属性数量的增加,性能表现良好(不会出现阶乘或组合爆炸等情况)。

以下是一种编写方式:

type NotEmpty<T> = T &
  { [K in keyof T]-?: { [P in K]-?: T[K] } }[keyof T];

所以NotEmpty<T>是与某些东西交叉的,因此我们知道,至少,NotEmpty<T>T的子类型。我们与之交叉的东西是一个分布式对象类型(在microsoft/TypeScript#47109中提出的术语),它是一个映射类型,我们立即使用它的所有键进行索引以获得一个联合。在上面的代码中,我们对keyof T中的每个K都获得了{[P in K]-?: T[K]}的联合。

而这个类型{[P in K]-?: T[K]}等效于Required<Pick<T, K>>(请参见RequiredPick实用类型):一个明确具有从T中的属性K的对象。

因此,整个过程是这样的:NotEmpty<T>T,同时也知道具有T中属性的某些已定义属性。


让我们在你的示例上测试一下:

type NotEmptyT = NotEmpty<T>;
/* type NotEmptyT = T & ({ x: number; } | { y: string; } | { z: number; }) */

看起来不错,让我们确保它按预期运行:

let a: NotEmpty<T>;
a = { z: 1 }; // 可行
a = {}; // 错误

看起来也很不错!

英文:

The union you're talking about of "all combinations with at least one required property" isn't too bad to write, especially since you can write the NotEmpty&lt;T&gt; utility type to compute it automatically. And that type only has one union member per property key, so it scales well with number of properties (it's not a factorial or combinatorial explosion or anything).

Here's one way to write it:

type NotEmpty&lt;T&gt; = T &amp;
  { [K in keyof T]-?: { [P in K]-?: T[K] } }[keyof T];

So a NotEmpty&lt;T&gt; is T intersected with something, so we know that, at minimum, NotEmpty&lt;T&gt; is a subtype of T. The thing we intersect it with,

{ [K in keyof T]-?: { [P in K]-?: T[K] } }[keyof T]

is a distributive object type (as coined in microsoft/TypeScript#47109), a mapped type into which we immediately index with all of its keys to get a union. In the above, we get the union of {[P in K]-?: T[K]} for every K in keyof T.

And that type {[P in K]-?: T[K]} is equivalent to Required&lt;Pick&lt;T, K&gt;&gt; (see Required and Pick utility types): an object which definitely has a defined K property from T.

So the whole thing is then: NotEmpty&lt;T&gt; is a T which is also known to have some defined property from the properties in T.


Let's test it out on your example:

type NotEmptyT = NotEmpty&lt;T&gt;
/* type NotEmptyT = T &amp; ({ x: number; } | { y: string; } | { z: number; }) */

Looks good, let's make sure it behaves as desired:

let a: NotEmpty&lt;T&gt;;
a = { z: 1 }; // ok
a = {}; // error

Also looks good!

Playground link to code

huangapple
  • 本文由 发表于 2023年5月11日 10:24:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76223737.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定