Отображение между круговыми типами в TypeScript - PullRequest
1 голос
/ 16 марта 2020

Этот вопрос касается статического вывода сигнатуры типов времени выполнения (встречается в таких библиотеках, как zod и io-ts ).

Следующий пример может быть видел в действии на эту ссылку на игровую площадку TS .

Допустим, мы пытаемся смоделировать некоторую информацию о типах для использования во время выполнения. Для начала мы можем объявить следующее перечисление Type:

enum Type {
  Boolean = "Boolean",
  Int = "Int",
  List = "List",
  Union = "Union",
}

Наша система типов времени выполнения должна поддерживать логические значения, целые числа, объединения и списки.

Базовый тип выглядит следующим образом :

interface Codec<T extends Type> {
  type: T;
}

Булевы и целочисленные типы используют этот базовый тип следующим образом.

Булево:

class BooleanCodec implements Codec<Type.Boolean> {
  type = Type.Boolean as const;
}

Целое число:

class IntCodec implements Codec<Type.Int> {
  type = Type.Int as const;
}

Тип объединения принимает массив типов для объединения:

class UnionCodec<C extends Codec<Type>> implements Codec<Type.Union> {
  type = Type.Union as const;
  constructor(public of: C[]) {}
}

И тип списка принимает тип, из которого состоят его элементы:

class ListCodec<C extends Codec<Type>> implements Codec<Type.List> {
  type = Type.List as const;
  constructor(public of: C) {}
}

Давайте создадим список логических значений или целые числа:

const listOfBooleanOrIntCodec = new ListCodec(
  new UnionCodec([
    new BooleanCodec(),
    new IntCodec(),
  ]),
);

Это дает оценку следующему объекту:

{
  type: Type.List,
  of: {
    type: Type.Union,
    of: [
      {
        type: Type.Boolean,
      },
      {
        type: Type.Int,
      },
    ]
  }
}

Этот код c будет иметь подпись ListCodec<UnionCodec<BooleanCodec | IntCodec>>.

Мы могли бы даже видеть циклы в данном коде c, и поэтому отображение сигнатуры типа становится сложным. Как бы мы получили от вышеупомянутого до (boolean | number)[]? И учитывает ли это глубокое вложение кодеков?

Для BooleanCodec или IntCodec работать в обратном направлении довольно легко ... но декодирование UnionCodec и ListCodec должно быть рекурсивным. Я попробовал следующее:

type Decode<C extends Codec<Type>> =
  // if it's a list
  C extends ListCodec<Codec<Type>>
    ? // and we can infer what it's a list of
      C extends ListCodec<infer O>
      ? // and the elements are of type codec
        O extends Codec<Type>
        ? // recurse to get an array of the element(s') type
          Decode<O>[]
        : never
      : never
    : // if it's a union
    C extends UnionCodec<Codec<Type>>
    // and we can infer what it's a union of
    ? C extends UnionCodec<infer U>
      // and it's a union of codecs
      ? U extends Codec<Type>
        // recurse to return that type (which will be inferred as the union)
        ? Decode<U>
        : never
      : never
      // if it's a boolean codec
    : C extends BooleanCodec
    // return the boolean type
    ? boolean
    // if it's ant integer codec
    : C extends IntCodec
    // return the number type
    ? number
    : never;

К сожалению, ошибки с Type alias 'Decode' circularly references itself и Type 'Decode' is not generic.

Мне интересно, возможно ли выполнить sh этот тип циклического типа- mapping, а как можно заставить работать такую ​​утилиту как Decode? Любая помощь будет принята с благодарностью. Спасибо!

1 Ответ

1 голос
/ 17 марта 2020

Я обычно определяю типы и затем извлекаю из них обобщенный c код c, а не создаю кодеки явно.

Например: сначала определите ваши типы с некоторыми данными и закодируйте их отношения ( значения элемента списка и объединения):

type Type = Integer | List<any> | Union<any>;
interface Integer {
  type: 'integer';
}
interface List<T extends Type> {
  type: 'list';
  item: T;
}
type UnionValues = Type[];
interface Union<T extends UnionValues> {
  type: 'union';
  values: T;
}

Также полезно предоставить помощников для создания следующих типов:

const integer: Integer = { type: 'integer' };
const list = <T extends Type>(item: T): List<T> => ({
  type: 'list',
  item
});
const union = <T extends UnionValues>(...values: T): Union<T> => ({
  type: 'union',
  values
});

Затем можно написать рекурсивную функцию отображения типов. Это сопоставит Type с соответствующим JS типом:

type Decode<T> =
  // terminal recursion: Integer is represented as a number
  T extends Integer ? number :
  // extract the Item from the list and construct an Array recursively
  T extends List<infer I> ? Decode<I>[] :
  // union is an array of types, so loop through and decode them
  T extends Union<infer U> ? {
    [i in Extract<keyof U, number>]: Decode<U[i]>;
  }[[Extract<keyof U, number>]] :
  never
  ;

Определите ваш код c как чтение из Type => Value:

interface Codec<T extends Type, V> {
  type: T;
  read(value: any): V;
}

Написать функцию который сопоставляет экземпляр типа с его кодом c:

function codec<T extends Type>(type: T): Codec<T, Decode<T>> {
  // todo
}

Теперь вы можете безопасно отобразить между вашей системой типов и JS типами:

const i = codec(integer);
const number: number = i.read('1');

const l = codec(list(integer));
const numberArray: number[] = l.read('[1, 2]');

const u = codec(union(integer, list(integer)));
const numberOrArrayOfNumbers: number | number[] = u.read('1');

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

Целочисленный код c - это прямое отображение целого числа -> число.

class IntegerCodec implements Codec<Integer, number> {
  public readonly type: Integer = integer;

  public read(value: any): number {
    return parseInt(value, 10);
  }
}

ListCode c рекурсивно вычисляет отображение List -> ItemValue []

namespace Codec {
  // helper type function for grabbing the JS type from a Codec<any, any>
  export type GetValue<C extends Codec<any, any>> = C extends Codec<any, infer V> ? V : never;
}
// this is where we recurse and compute the Type and JSType from the provided Item codec
class ListCodec<Item extends Codec<any, any>> implements Codec<List<Item['type']>, Codec.GetValue<Item>[]> {
  public readonly type: List<Item['type']>;
  constructor(public readonly item: Item)  {
    this.type = list(item.type);
  }

  public read(value: any): Codec.GetValue<Item>[] {
    return value.map((v: any) => this.item.read(v));
  }
}

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

Первая утилита: Compute Тип объединения из кортежа кодеков

type ComputeUnionType<V extends Codec<any, any>[]> = Union<Type[] & {
  [i in Extract<keyof V, number>]: V[i]['type']
}>;

Вторая утилита: Вычисление типа объединения JS из кортежа кодеков:

type ComputeUnionValue<V extends Codec<any, any>[]> = {
  [i in Extract<keyof V, number>]: Codec.GetValue<V[i]>;
}[Extract<keyof V, number>];

Затем мы пишем код UnionCode c, который рекурсивно вычисляет тип и JS тип объединения:

class UnionCodec<V extends Codec<any, any>[]> implements Codec<
  ComputeUnionType<V>,
  ComputeUnionValue<V>
> {
  public readonly type: ComputeUnionType<V>;

  constructor(public readonly codecs: V) {}
  public read(value: any): ComputeUnionValue<V> {
    throw new Error("Method not implemented.");
  }
}

Теперь ваш пример проверки типов:

const ic = new IntegerCodec();
const lc: ListCodec<IntegerCodec> = new ListCodec(new IntegerCodec());
const uc: UnionCodec<[ListCodec<IntegerCodec>, IntegerCodec]> = new UnionCodec([lc, ic]);

const listValue: number | number[] = uc.read('1');
...