Моя цель
У меня есть строка enum E
и interface I
с идентичными наборами ключей.Я хочу создать новый сопоставленный тип.Для каждого общего ключа k
в качестве имени свойства следует использовать значение enum E.k
.Тип члена I.k
должен быть типом этого нового свойства.
Некоторый фон / мотивация для моего варианта использования
Я получаю объекты из REST API.Я не могу изменить их структуру.Имена ключей объектов очень нечитаемы и уродливы из-за устаревших причин (я моделирую это в FooNames
в примере).Это делает разработку болезненной и излишне увеличивает количество ошибок как в коде, так и более критично для понимания при работе с этими объектами и манипулировании ими.
Мы скрыли эти имена, используя собственный чистый интерфейс (смоделированный с помощью "first" | "second" | "third"
).Однако при записи объектов обратно в бэкэнд им необходимо снова иметь «некрасивую» структуру.Существует несколько десятков типов объектов (с разными наборами полей в каждом), что делает работу с запутанными именами полей настолько болезненной.
Мы пытаемся минимизировать избыточность - при этом все еще проводим проверку статического типа и структурычерез компилятор TS.Таким образом, сопоставленный тип, который запускает проверки типов на основе существующих абстракций, был бы очень полезен.
Пример кода
Может ли приведенный ниже тип BackendObject
быть каким-то образом реализован как отображаемый тип вМашинопись? Мне пока не удалось найти способ.См. эту площадку для всего кода в этом вопросе.
// 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.Enum (Имена) + Интерфейс (типы)
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'
}
}