我的目标
我有一个字符串 enum E
和一个 interface I
具有相同的键集。我想构造一个新的映射类型。对于每个共享 key k
,它应该使用枚举值 E.k
作为属性名称。成员 I.k
的类型应该是这个新属性的类型。
我的用例的一些背景/动机
我从 REST API 获取对象。我不能改变他们的结构。由于遗留原因,对象的键名称非常难读且难看(我在示例中的 FooNames
中模拟了这一点)。
这使得开发变得痛苦,并且在代码中增加了不必要的错误,但在使用和操作这些对象时更重要的是在理解方面。
我们使用我们自己的干净界面隐藏了这些名称(通过 via "first"| "second"| "third"
模拟)。然而,当对象回写到后端时,他们需要再次拥有“丑陋”的结构。
有几十种对象类型(每种类型都有不同的字段集),这使得处理令人困惑的字段名称变得如此痛苦。
我们正在尝试尽量减少冗余 - 同时仍然通过 TS 编译器进行静态类型和结构检查。因此,基于现有抽象触发类型检查的映射类型将非常有帮助。
代码示例
下面的 BackendObject
类型能否以某种方式实现为 Typescript 中的映射类型?到目前为止,我还没有找到方法。参见 this playground对于这个问题中的所有代码。
// Two simple abstractions per object type, e.g. for a type Foo....
enum FooNames {
first = 'FIRST_FIELD',
second = 'TT_FIELD_SECOND',
third = 'third_field_33'
}
interface FooTypes {
first: string,
second: number,
third: boolean
}
// ... allow for generic well-formed objects with structure and typechecks:
interface FrontendObject<FieldNames extends keyof FieldTypes, FieldTypes> {
fields: {[K in FieldNames]: FieldTypes[K]}
}
// Example object in the case of our imaginary type "Foo":
let checkedFooObject: FrontendObject<keyof typeof FooNames,FooTypes> = {
fields: {
first: '', // typechecks everywhere!
second: 5,
third: false,
// extraProp: 'this is also checked and disallowed'
}
}
// PROBLEM: The following structure is required to write objects back into database
interface FooBackendObject {
fields: {
FIRST_FIELD: string,
TT_FIELD_SECOND_TT: number,
third_field_33: boolean
// ...
// Adding new fields manually is cumbersome and error-prone;
// critical: no static structure or type checks available
}
}
// IDEAL GOAL: Realize this as generic mapped type using the abstractions above like:
let FooObjectForBackend: BackendObject<FooNames,FooTypes> = {
// build the ugly object, but supported by type and structure checks
};
到目前为止我的尝试
1。枚举(名称)+ 接口(interface)(类型)
interface BackendObject1<FieldNames extends string, FieldTypes> {
fields: {
// FieldTypes cannot be indexed by F, which is now the ugly field name
[F in FieldNames]: FieldTypes[F];
// Syntax doesn't work; no reverse mapping in string-valued enum
[F in FieldNames]: FieldTypes[FieldNames.F];
}
}
// FAILURE Intended usage:
type FooObjectForBackend1 = BackendObject1<FooNames,FooTypes>;
2。改为使用丑陋的键进行字段类型抽象
interface FooTypes2 {
[FooNames.first]: string,
[FooNames.second]: number,
[FooNames.third]: boolean,
}
// SUCCESS Generic backend object type
interface BackendObject2<FieldNames extends keyof FieldTypes, FieldTypes> {
fields: {
[k in FieldNames]: FieldTypes[k]
}
}
// ... for our example type Foo:
type FooBackend = BackendObject2<FooNames, FooTypes2>
let someFooBackendObject: FooBackend = {
fields: {
[FooNames.first]: 'something',
[FooNames.second]: 5,
[FooNames.third]: true
}
}
// HOWEVER.... Generic frontend object FAILURE
interface FrontendObject2<NiceFieldNames extends string, FieldNames extends keyof FieldTypes, FieldTypes> {
fields: {
// Invalid syntax; no way to access enum and no matching of k
[k in NiceFieldNames]: FieldTypes[FieldNames.k]
}
}
3。将对象抽象组合为元组,使用字符串文字类型
// Field names and types in one interface:
interface FooTuples {
first: ['FIRST_FIELD', string]
second: ['TT_FIELD_SECOND', number]
third: ['third_field_33', boolean]
}
// FAILURE
interface BackendObject3<TypeTuples> {
fields: {
// e.g. { first: string }
// Invalid syntax for indexing
[k in TypeTuples[1] ]: string|number|boolean
}
}
4。每种类型一个“字段”对象
// Abstractions for field names and types combined into a single object
interface FieldsObject {
fields: {
[niceName: string]: {
dbName: string,
prototype: string|boolean|number // used only for indicating type
}
}
}
let FooFields: FieldsObject = {
fields: {
first: {
dbName: 'FIRST_FIELD',
prototype: ''
},
second: {
dbName: 'TT_FIELD_SECOND',
prototype: 0
},
third: {
dbName: 'third_field3',
prototype: true,
}
}
}
// FAIL: Frontend object type definition
interface FrontendObject3<FieldsObject extends string> {
fields: {
// Cannot access nested type of 'prototype'
[k in keyof FieldsObject]: FieldsObject[k][prototype];
}
}
// FAIL: Backendobject type definition
interface BackendObject3<FieldsObject extends string> {
fields: {
[k in keyof ...]: // No string literal type for all values of 'dbName'
}
}
最佳答案
我认为以下内容应该适合您:
type BackendObject<
E extends Record<keyof E, keyof any>,
I extends Record<keyof E, any>
> = {
fields: {
[P in E[keyof E]]: I[{
[Q in keyof E]: E[Q] extends P ? Q : never
}[keyof E]]
}
}
interface FooBackendObject extends
BackendObject<typeof FooNames, FooTypes> { }
类型 BackendObject<E, I>
不是接口(interface),但您可以为 E
和 I
的任何特定具体值声明接口(interface),如上面的 FooBackendObject
所示。因此,在 BackendObject<E, I>
中,我们期望 E
是到键的映射(在 FooBackendObject
中由 FooNames
value 表示,其类型为 typeof FooNames
...你不能只使用FooNames
类型 ,因为 doesn't contain the mapping 。)和 I
是到值的映射(在 FooBackendObject
中由接口(interface) FooTypes
表示)。
使用的映射/条件类型可能有点难看,但这就是我们正在做的:首先,fields
对象的键来自 E
( E[keyof E]
) 的值。对于其中的每个键P
,我们找到对应的E
的键({[Q in keyof E]: E[Q] extends P ? Q : never}[keyof E]
),然后使用该键索引到I
作为值类型。
让我们更全面地解释 {[Q in keyof E]: E[Q] extends P ? Q : never}[keyof E]
。通常,像 {[Q in keyof E]: SomeType<Q>}[keyof E]
这样的类型将是 SomeType<Q>
中所有 Q
的 keyof E
的并集。如果这更有意义,你可以用具体类型兑现......如果 E
是 {a: string, b: number}
,那么 {[Q in keyof E]: SomeType<Q>}
将是 {a: SomeType<'a'>, b: SomeType<'b'>}
,然后我们 look up 它在键 keyof E
的值,即 {a: SomeType<'a'>, b: SomeType<'b'>}['a'|'b']
,变成 64.7915在我们的例子中,SomeType<'a'> | SomeType<'b'>
是 SomeType<Q>
,如果 E[Q] extends P ? Q : never
匹配 Q
,则计算结果为 E[Q]
,否则为 P
。因此,我们得到 never
中 Q
值的并集,其中 keyof E
与 E[Q]
匹配。应该只有其中之一(如果枚举没有两个具有相同值的键)。
完成手动评估 P
的练习以查看它的发生可能对您很有用。
您可以验证它是否按预期运行。希望有所帮助。祝你好运!
关于typescript - 在 Typescript 中优雅地将两种类型组合成一个接口(interface),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55208690/