Сопоставить массив объекту с правильными типами (только тип) - PullRequest
0 голосов
/ 03 апреля 2020

Введение

В нашем проекте есть поддержка атрибутов, где каждый атрибут является классом. Атрибут содержит информацию о типе, опциональности и имени. Вместо определения интерфейса для каждой сущности, я хотел бы автоматизировать его. У нас есть около 500 атрибутов и более 100 объектов. Сущность является сборщиком атрибутов.

Пример

interface AttributeClass {
  readonly attrName: string;
  readonly required?: boolean;
  readonly valueType: StringConstructor | NumberConstructor  | BooleanConstructor;
}

class AttrTest extends Attribute {
  static readonly attrName = "test";
  static readonly required = true;
  static readonly valueType = String
}

class Attr2Test extends Attribute {
  static readonly attrName = "test2";
  static readonly valueType = Number
}

interface Entity {
  test: string // AttrTest
  test2?: number // Attr2Test
}

class SomeClass {
  static attributes = [AttrTest, Attr2Test]
}

Здесь вы можете заметить, что у меня есть valueType, который содержит реальный тип. Я также знаю имя, и если это необязательно. (требуется, если required существует и имеет значение true)

Концепция и мое не работающее решение

Моя идея состоит в том, чтобы перебрать массив attributes, сопоставить значение с именем и сделайте его необязательным.

  1. Введите для фильтрации необязательный атрибут
export type ValueOf<T> = T[keyof T];
type FilterOptionalAttribute<Attr extends AttributeClass> = ValueOf<Attr["required"]> extends false | undefined | null ? Attr : never
Введите для фильтрации требуемый атрибут
type FilterRequiredAttribute<Attr extends AttributeClass> = FilterOptionalAttribute<Attr> extends never ? Attr : never
Тип для преобразования из Типа в тип примитива
type ExtractPrimitiveType<A> =
  A extends StringConstructor ? string :
    A extends NumberConstructor ? number :
      A extends BooleanConstructor ? boolean :
        never
Тип для преобразования из класса в объект значения ключа (обязательно + необязательно)
type AttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]: ExtractPrimitiveType<Attr["valueType"]> }

type OptionalAttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]?: ExtractPrimitiveType<Attr["valueType"]> }
Склейте это вместе + что-то, чтобы вывести тип массива
type UnboxAttributes<AttrList> = AttrList extends Array<infer U> ? U : AttrList;

type DataType<AttributeList extends AttributeClass[]> = OptionalAttributeDataType<FilterOptionalAttribute<UnboxAttributes<AttributeList>>> & AttributeDataType<FilterRequiredAttribute<UnboxAttributes<AttributeList>>>

Что я ожидаю на выходе

class SomeClass {
  static attributes = [AttrTest, Attr2Test]
}

// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
  test: string
  test2?: number
}

Что показывает IDE

используя IntelliJ IDEA Ultimate

// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
  test: string | number
  test2: number | number
}

Я потратил уже 5 часов на ее решение. Кажется, мне не хватает чего-то важного. Я хотел бы поблагодарить всех, кто дает мне какие-либо советы, что я делаю неправильно.

Есть две проблемы:

  • Все требуется (test2 должен быть необязательным)
  • Типы смешиваются, даже когда я их выводю

Ссылка для TypeScript Playground

Ответы [ 2 ]

1 голос
/ 03 апреля 2020

Я собираюсь ответить на урезанную версию вопроса, которая игнорирует определенные определения c класса и разницу между конструкторами со свойствами и экземплярами stati c. Вы можете использовать общую технику, представленную ниже, в полной версии с соответствующими преобразованиями.

Учитывая следующий интерфейс,

interface AttributeInterface {
  attrName: string;
  required?: boolean;
  valueType: StringConstructor | NumberConstructor | BooleanConstructor;
}

Я представлю DataType<T extends AttributeInterface>, который преобразует T, a объединение из AttributeInterface s, к сущности, которую оно представляет. Обратите внимание, что если у вас есть тип массива Arr, например [Att1, Att2], вы можете превратить его в объединение, просмотрев его number сигнатуру индекса: Arr[number] равно Att1 | Att2.

В любом случае, вот оно:

type DataType<T extends AttributeInterface> = (
  { [K in Extract<T, { required: true }>["attrName"]]:
    ReturnType<Extract<T, { attrName: K }>["valueType"]> } &
  { [K in Exclude<T, { required: true }>["attrName"]]?:
    ReturnType<Extract<T, { attrName: K }>["valueType"]> }
) extends infer O ? { [K in keyof O]: O[K] } : never;

Прежде чем я объясню это, давайте попробуем это на следующих двух интерфейсах:

interface AttrTest extends AttributeInterface {
  attrName: "test";
  required: true;
  valueType: StringConstructor
}

interface Attr2Test extends AttributeInterface {
  attrName: "test2";
  valueType: NumberConstructor;
}

type Entity = DataType<AttrTest | Attr2Test>;
/* type Entity = {
    test: string;
    test2?: number | undefined;
} */

Хорошо выглядит.


Итак, объяснение: я беру объединение атрибутов T и делю его на две части: обязательные атрибуты Extract<T, { required: true }> и необязательные атрибуты Exclude<T, { required: true }>, где Extract и Exclude являются типами утилит, которые фильтруют объединения.

Единственная разница между обработкой, выполненной для этих двух частей, заключается в том, что отображенный тип для первого требуется (нет ? в определении), а отображаемый тип для последнего является необязательным (с ? в определении), по желанию ... Затем я пересекаю их вместе.

В любом случае, для каждого ключа K в свойстве attrName этих частей T значение свойства i тип s ReturnType<Extract<T, { attrName: K }>["valueType"]>. Extract<T, {attrName: K}> просто находит одного члена T с K в качестве attrName. Затем мы ищем его свойство "valueType", которое, как мы знаем, является одним (или более) из StringConstructor, NumberConstructor, BooleanConstructor.

Оказывается, что каждый из этих типов является вызываемой функцией, которая возвращает примитивный тип данных:

const s: string = String(); // StringConstructor's return type is string
const n: number = Number(); // NumberConstructor's return type is number
const b: boolean = Boolean(); // BooleanConstructor's return type is boolean

, что означает, что мы можем легко получить примитивный тип, используя ReturnType тип утилиты.

Единственное, что осталось объяснить, это ... extends infer O ? { [K in keyof O]: O[K] } : never. Это трюк, чтобы взять тип пересечения, такой как {foo: string} & {bar?: number}, и превратить его в один тип объекта, такой как {foo: string; bar?: number}.


Опять же, легко преобразовать это в форму, которая принимает тип массива :

type DataTypeFromArray<T extends AttributeInterface[]> = DataType<T[number]>;

type AlsoEntity = DataTypeFromArray<[AttrTest, Attr2Test]>;
/* type AlsoEntity = {
    test: string;
    test2?: number | undefined;
} */

Что должно помочь в создании решения для классов в вашем примере кода.


Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

1 голос
/ 03 апреля 2020

У меня есть другое, но сработавшее решение ...

// Declare constructor type
type Constructor<T> = new (...args: any[]) => T;

// Declare support attribute types
type SupportTypes = [String, Number, Boolean];

// Attribyte class
class AttributeClass<K extends string, T extends SupportTypes[number], R extends boolean = false> {
  constructor(
    readonly attrName: K,
    readonly valueType: Constructor<T>,
    readonly required?: R,
  ) {
  }
}


// Declare test attributes
const AttrTest = new AttributeClass('test', String, true);
const Attr2Test = new AttributeClass('test2', Number);

const attributes = [AttrTest, Attr2Test];

// Unwrap instance of AttributeClass, to object
type UnwrapAttribute<T> = T extends AttributeClass<infer K, infer T, infer R> ? (
  R extends true ? {
    [key in K]: T;
  } : {
    [key in K]?: T;
  }
) : never;

// Transform union to intersection
// Example: UnionToIntersection<{a: string} | {b: number}> => {a: string, b: number}
type UnionToIntersection<U> = ((U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never);

// Transform tuple to intersection
// Example: TupleToIntersection<[{a: string}, {b: number}]> => {a: string, b: number}
type TupleToIntersection<U extends Array<any>> = UnionToIntersection<U[number]>;

// Map array of attributes
type MapAttributes<ArrT extends Array<AttributeClass<any, any, any>>> = TupleToIntersection<{
  [I in keyof ArrT]: UnwrapAttribute<ArrT[I]>;
}>;

// Result
const mapped: MapAttributes<typeof attributes> = {
  test: '123',
  test2: 123,
};

Детская площадка

...