Как описать отношения между элементами кортежа в массиве кортежей через типы в TypeScript? - PullRequest
0 голосов
/ 17 мая 2019

Я хочу описать отношения между элементами кортежа в массиве кортежей через типы в TypeScript.

Возможно ли это?

declare const str: string;
declare const num: number;
function acceptString(str: string) { }
function acceptNumber(num: number) { }

const arr /*: ??? */ = [
    [str, acceptString],
    [num, acceptNumber],
    [str, acceptNumber], // should be error
];

for (const pair of arr) {
    const [arg, func] = pair;
    func(arg); // should be no error
}

Ссылка на TypeScript Playground

Пример из реальной жизни: Ссылка на TypeScript Playground

1 Ответ

2 голосов
/ 17 мая 2019

Вы в основном просите что-то, что я звонил коррелированные типы записей , для которых в настоящее время нет прямой поддержки в TypeScript. Даже в том случае, когда вы можете убедить компилятор перехватывать ошибки при создании таких записей, он на самом деле не способен проверять безопасность типов при его использовании.

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

type MyRecord<T> = [T, (arg: T)=>void];
type SomeMyRecord = <exists T> MyRecord<T>; // this is not valid syntax
type ArrayOfMyRecords = Array<SomeMyRecord>;

Следующая лучшая вещь может состоять в том, чтобы позволить ArrayOfMyRecords самому себе быть универсальным типом, в котором каждый элемент массива строго типизирован с его аналогичным значением T, и вспомогательной функцией для вывода более сильного типа:

type MyRecord<T> = [T, (arg: T) => void];

const asMyRecordArray = <A extends any[]>(
  a: { [I in keyof A]: MyRecord<A[I]> } | []
) => a as { [I in keyof A]: MyRecord<A[I]> };

Используется вывод из отображенных типов и сопоставленных кортежей . Давайте посмотрим на это в действии:

const arr = asMyRecordArray([
  [str, acceptString],
  [num, acceptNumber],
  [str, acceptNumber] // error
]);
// inferred type of arr:
// const arr:  [
//   [string, (arg: string) => void], 
//   [number, (arg: number) => void], 
//   [string, (arg: string) => void]
// ]

Давайте исправим это:

const arr = asMyRecordArray([
  [str, acceptString],
  [num, acceptNumber],
  [str, acceptString] 
]);
// inferred type of arr:
// const arr:  [
//   [string, (arg: string) => void], 
//   [number, (arg: number) => void], 
//   [string, (arg: string) => void]
// ]

Так что это достаточно хорошо, чтобы определить arr. Но теперь посмотрим, что происходит, когда вы перебираете его:

// TS3.3+ behavior
for (const pair of arr) {
  const [arg, func] = pair; 
  func(arg); // still error!
}

Это то, где отсутствие поддержки коррелированных записей обжигает вас. В TypeScript 3.3 была добавлена ​​поддержка для вызова объединений типов функций , но эта поддержка не затрагивает эту проблему, а именно: компилятор обрабатывает func как объединение функций, которые полностью некоррелированный с типом arg. Когда вы вызываете его, компилятор решает, что он может безопасно принимать только аргументы типа string & number, который arg не является (и не является действительным значением, поскольку string & number сворачивается в never).

Так что, если вы пойдете этим путем, вы обнаружите, что вам нужно утверждение типа , чтобы успокоить компилятор:

for (const pair of arr) {
    const [arg, func] = pair as MyRecord<string | number>;
    func(arg); // no error now
    func(12345); // no error here either, so not safe
}

Кто-то может решить, что это лучшее, что вы можете сделать, и оставить его там.


Теперь, есть способ кодировать экзистенциальные типы в TypeScript, но он включает Promise -подобную инверсию управления. Прежде чем мы пойдем по этому пути, спросите себя: что вы собираетесь на самом деле делать с MyRecord<T>, когда вы не знаете T? Единственное разумное, что вы можете сделать, это вызвать первый элемент вместе со вторым элементом. И если это так, вы можете дать более конкретный метод, который просто делает это без учета T:

type MyRecord<T> = [T, (arg: T) => void];
type MyUsefulRecord<T> = MyRecord<T> & { callFuncWithArg(): void };

function makeUseful<T>(arg: MyRecord<T>): MyUsefulRecord<T> {
    return Object.assign(arg, { callFuncWithArg: () => arg[1](arg[0]) });
}

const asMyUsefulRecordArray = <A extends any[]>(
    a: { [I in keyof A]: MyUsefulRecord<A[I]> } | []
) => a as { [I in keyof A]: MyUsefulRecord<A[I]> };

const arr = asMyUsefulRecordArray([
    makeUseful([str, acceptString]),
    makeUseful([num, acceptNumber]),
    makeUseful([str, acceptString])
]);

for (const pair of arr) {
    pair.callFuncWithArg(); // okay!
}

Ваш реальный пример может быть изменен аналогично:

function creatify<T, U>(arg: [new () => T, new (x: T) => U]) {
    return Object.assign(arg, { create: () => new arg[1](new arg[0]()) });
}

const map = {
    [Type.Value1]: creatify([Store1, Form1]),
    [Type.Value2]: creatify([Store2, Form2])
};

function createForm(type: Type) {
    return map[type].create();
}

Эмуляция экзистенциальных типов в TypeScript аналогична вышеописанной, за исключением того, что она позволяет вам делать абсолютно все с MyRecord<T>, что можно сделать, если вы не знаете T. Поскольку в большинстве случаев это небольшой набор операций, зачастую проще просто поддерживать их напрямую.


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

...