如何在 TypeScript 中键入具有已知和未知键的对象 [英] How do I type an object with known and unknown keys in TypeScript
问题描述
我正在寻找一种方法来为以下具有两个已知键和一个已知类型的未知键的对象创建 TypeScript 类型:
I am looking for a way to create TypeScript types for the following object that has two known keys and one unknown key that has a known type:
interface ComboObject {
known: boolean
field: number
[U: string]: string
}
const comboObject: ComboObject = {
known: true
field: 123
unknownName: 'value'
}
该代码不起作用,因为 TypeScript 要求所有属性都匹配给定索引签名的类型.但是,我不想使用索引签名,我想键入一个我知道其类型但不知道其名称的字段.
That code does not work because TypeScript requires that all properties match the type of the given index signature. However, I am not looking to use index signatures, I want to type a single field where I know its type but I do not know its name.
到目前为止我唯一的解决方案是使用索引签名并设置所有可能类型的联合类型:
The only solution I have so far is to use index signatures and set up a union type of all possible types:
interface ComboObject {
[U: string]: boolean | number | string
}
但这有很多缺点,包括允许已知字段的类型不正确以及允许任意数量的未知键.
But that has many drawbacks including allowing incorrect types on the known fields as well as allowing an arbitrary number of unknown keys.
有更好的方法吗?TypeScript 2.8 条件类型有什么帮助吗?
Is there a better approach? Could something with TypeScript 2.8 conditional types help?
推荐答案
你要求的.
让我们做一些类型操作来检测给定的类型是否是联合.它的工作方式是使用 distributive 条件类型的属性将联合展开到组成部分,然后注意每个组成部分比联合更窄.如果这不是真的,那是因为联合只有一个组成部分(所以它不是联合):
You asked for it.
Let's do some type manipulation to detect if a given type is a union or not. The way it works is to use the distributive property of conditional types to spread out a union to constituents, and then notice that each constituent is narrower than the union. If that isn't true, it's because the union has only one constituent (so it isn't a union):
type IsAUnion<T, Y = true, N = false, U = T> = U extends any
? ([T] extends [U] ? N : Y)
: never;
然后用它来检测给定的 string
类型是否是单个字符串文字(所以:不是 string
,不是 never
,也不是一个联合):
Then use it to detect if a given string
type is a single string literal (so: not string
, not never
, and not a union):
type IsASingleStringLiteral<
T extends string,
Y = true,
N = false
> = string extends T ? N : [T] extends [never] ? N : IsAUnion<T, N, Y>;
现在我们可以开始处理您的特定问题了.将 BaseObject
定义为 ComboObject
的一部分,您可以直接定义:
Now we can start taking about your particular issue. Define BaseObject
as the part of ComboObject
that you can define straightforwardly:
type BaseObject = { known: boolean, field: number };
并为错误消息做准备,让我们定义一个 ProperComboObject
以便当您搞砸时,错误会提示您应该做什么:
And preparing for error messages, let's define a ProperComboObject
so that when you mess up, the error gives some hint about what you were supposed to do:
interface ProperComboObject extends BaseObject {
'!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!': string
}
主菜来了.VerifyComboObject<C>
接受一个类型 C
并且如果它符合您想要的 ComboObject
类型,则将它原封不动地返回;否则它会返回 ProperComboObject
(它也不会符合)出现错误.
Here comes the main course. VerifyComboObject<C>
takes a type C
and returns it untouched if it conforms to your desired ComboObject
type; otherwise it returns ProperComboObject
(which it also won't conform to) for errors.
type VerifyComboObject<
C,
X extends string = Extract<Exclude<keyof C, keyof BaseObject>, string>
> = C extends BaseObject & Record<X, string>
? IsASingleStringLiteral<X, C, ProperComboObject>
: ProperComboObject;
它的工作原理是将 C
分解为 BaseObject
和剩余的键 X
.如果 C
不匹配 BaseObject &记录<X, string>
,那么你失败了,因为这意味着它要么不是一个 BaseObject
,要么是一个具有额外的非 string
属性的.然后,它通过检查 X
和 IsASingleStringLiteral
来确保 正好有一个剩余的密钥.
It works by dissecting C
into BaseObject
and the remaining keys X
. If C
doesn't match BaseObject & Record<X, string>
, then you've failed, since that means it's either not a BaseObject
, or it is one with extra non-string
properties. Then, it makes sure that there is exactly one remaining key, by checking X
with IsASingleStringLiteral<X>
.
现在我们创建一个辅助函数,它要求输入参数匹配VerifyComboObject
,并返回不变的输入.如果您只想要一个正确类型的对象,它可以让您及早发现错误.或者你可以使用签名来帮助你自己的函数需要正确的类型:
Now we make a helper function which requires that the input parameter match VerifyComboObject<C>
, and returns the input unchanged. It lets you catch mistakes early if you just want an object of the right type. Or you can use the signature to help make your own functions require the right type:
const asComboObject = <C>(x: C & VerifyComboObject<C>): C => x;
让我们测试一下:
const okayComboObject = asComboObject({
known: true,
field: 123,
unknownName: 'value'
}); // okay
const wrongExtraKey = asComboObject({
known: true,
field: 123,
unknownName: 3
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing
const missingExtraKey = asComboObject({
known: true,
field: 123
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing
const tooManyExtraKeys = asComboObject({
known: true,
field: 123,
unknownName: 'value',
anAdditionalName: 'value'
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing
第一个编译,根据需要.最后三个失败的原因不同,与额外属性的数量和类型有关.错误消息有点神秘,但这是我能做的最好的.
The first one compiles, as desired. The last three fail for different reasons having to do with the number and type of extra properties. The error message is a little cryptic, but it's the best I can do.
可以看到活动的代码在操场.
You can see the code in action in the Playground.
同样,我不认为我建议将其用于生产代码.我喜欢使用类型系统,但这个系统感觉特别复杂和脆弱,我不会不想对任何不可预见的后果负责.
Again, I don't think I recommend that for production code. I love playing with the type system, but this one feels particularly complicated and fragile, and I wouldn't want to feel responsible for any unforeseen consequences.
希望对你有帮助.祝你好运!
Hope it helps you. Good luck!
这篇关于如何在 TypeScript 中键入具有已知和未知键的对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!