Max*_*Hax 3 generics types typescript typescript-generics
我有一个字符串,enum E并且具有一interface I组相同的键。我想构造一个新的映射类型。对于每个共享密钥k,它应使用枚举值E.k作为属性名称。成员的类型I.k应该是此新属性的类型。
我从REST API获取对象。我无法改变他们的结构。由于遗留原因,对象的键名非常难以阅读且难看(我FooNames在示例中对此进行了模拟)。这使开发过程痛苦不堪,并不必要地增加了代码错误,但在使用这些对象并对其进行操作时,在理解上却更为关键。
我们使用自己的干净接口(通过via模拟"first" | "second" | "third")隐藏了这些名称。但是,将对象写回后端时,它们需要再次具有“丑陋”的结构。对象类型有几十种(每种类型具有不同的字段集),这使得处理混乱的字段名称变得如此痛苦。
我们正在尝试最小化冗余-同时仍通过TS编译器进行静态类型和结构检查。因此,基于现有抽象触发类型检查的映射类型将非常有帮助。
可以将BackendObject以下类型以某种方式在Typescript中实现为映射类型吗?到目前为止,我还没有找到方法。有关此问题中的所有代码,请参见此操场。
// 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
};
Run Code Online (Sandbox Code Playgroud)
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>;
Run Code Online (Sandbox Code Playgroud)
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]
}
}
Run Code Online (Sandbox Code Playgroud)
// 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
}
}
Run Code Online (Sandbox Code Playgroud)
// 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'
}
}
Run Code Online (Sandbox Code Playgroud)
我认为以下应为您工作:
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> { }
Run Code Online (Sandbox Code Playgroud)
的类型BackendObject<E, I>是不是一个接口,但可以声明一个接口,用于任何特定的具体值E和I如FooBackendObject以上。所以,BackendObject<E, I>我们希望E成为映射键(在代表FooBackendObject由FooNames 值,其类型是typeof FooNames...你不能只使用FooNames 类型在这里,因为这不包含映射),并I成为映射到值(FooBackendObject由接口表示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]将是in SomeType<Q>中所有的Q并集keyof E。如果这更有意义,你可以用一个具体类型兑现了...如果E是{a: string, b: number},那么{[Q in keyof E]: SomeType<Q>}会{a: SomeType<'a'>, b: SomeType<'b'>},然后我们查找的键的值keyof E,也就是{a: SomeType<'a'>, b: SomeType<'b'>}['a'|'b'],成为SomeType<'a'> | SomeType<'b'>。在我们的例子,SomeType<Q>就是E[Q] extends P ? Q : never,计算结果为Q是否E[Q]匹配P,以及never其他。因此,我们得到的工会Q值keyof E为其E[Q]匹配P。应该只有其中之一(如果枚举没有两个具有相同值的键)。
对您进行手动评估BackendObject<typeof FooNames, FooTypes>以了解它的发生可能对您很有用。
您可以验证其行为是否符合预期。希望能有所帮助。祝好运!
| 归档时间: |
|
| 查看次数: |
853 次 |
| 最近记录: |