英文:
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<T> = { 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>>
(请参见Required
和Pick
实用类型):一个明确具有从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<T>
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<T> = T &
{ [K in keyof T]-?: { [P in K]-?: T[K] } }[keyof T];
So a NotEmpty<T>
is T
intersected with something, so we know that, at minimum, NotEmpty<T>
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<Pick<T, K>>
(see Required
and Pick
utility types): an object which definitely has a defined K
property from T
.
So the whole thing is then: NotEmpty<T>
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<T>
/* type NotEmptyT = T & ({ x: number; } | { y: string; } | { z: number; }) */
Looks good, let's make sure it behaves as desired:
let a: NotEmpty<T>;
a = { z: 1 }; // ok
a = {}; // error
Also looks good!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论