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

#typescript #typescript-typings #typescript-generics

#typescript #typescript-типизации #typescript-generics

Вопрос:

У меня есть одно значение некоторого типа кортежа, и я хочу сопоставить его с другим кортежем другого типа. Кортежи могут быть разнородными. Есть ли способ выполнить это сопоставление без использования приведения типа в примере ниже?

 interface SomeType<T> {
  value: T
}

type SomeTypeList<T extends any[]> = { [K in keyof T]: SomeType<T[K]> }

interface OtherType<T> {
  otherValue: T
}

type OtherTypeList<T extends any[]> = { [K in keyof T]: OtherType<T[K]> }

function convertValues<T extends any[]> (arr: SomeTypeList<T>): OtherTypeList<T> {
  // Is there any way to write this without the cast?
  return arr.map(({ value }) => ({ otherValue: value })) as OtherTypeList<T>
}

convertValues<[number, string, boolean]>([{ value: 1 }, { value: 'cat' }, { value: true }])
// => [{ otherValue: 1 }, { otherValue: 'cat' }, { otherValue: true }]
 

При приведении он теряет безопасность типов, поскольку вместо этого может вернуться обратный ({ otherValue: 1 }) вызов, и все будет проверять правильность ввода.

Ответ №1:

Нет, это невозможно в TypeScript с TS4.1. Я вижу две проблемы; одну можно преодолеть, изменив типизацию стандартной библиотеки TypeScript Array.prototype.map() , но для другой потребуется довольно большое изменение системы типов для работы в целом, и в настоящее время она не может быть обработана вспособ, который в некотором смысле не эквивалентен утверждению типа (то, что вы называете «приведением»).


Текущая типизация библиотеки для map() метода массива:

 interface Array<T> {
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
}
 

Возвращаемый тип — U[] это неупорядоченный тип массива, соответствующий типу вывода callbackfn аргумента. Это не кортеж. Существует открытая проблема GitHub, microsoft / TypeScript #29841 с просьбой изменить это, чтобы при вызове map() кортежа вы получали кортеж той же длины. В настоящее время это не реализовано; но вы можете использовать слияние объявлений, чтобы проверить такое изменение самостоятельно:

 interface Array<T> {
  map<This extends Array<T>, U>(this: This, fn: (v: T) => U): { [K in keyof This]: U }
}
 

Если я вызову convertValues() (с изменением arr параметра как типа переменного кортежа, который дает компилятору подсказку, что он должен интерпретировать arr как кортеж, если это возможно), вы можете увидеть, как теперь он знает, сколько элементов в возвращаемом значении:

 function convertValues<T extends any[]>(arr: [...SomeTypeList<T>]) {
  return arr.map(({ value }) => ({ otherValue: value }))
}

const ret = convertValues([{ value: 1 }, { value: 'cat' }, { value: true }])
ret[0]; ret[1]; ret[2]; // okay
ret[3] // error! Tuple type of length '3' has no element at index '3'.
 

К сожалению, типы каждого элемента кортежа неизвестны:

 ret[0].otherValue.toFixed(2); // error!
// -------------> ~~~~~~~
// Property 'toFixed' does not exist on type 'string | number | boolean'.
 

Каждый элемент известен только компилятору {otherValue: string | number | boolean} как . Это не неверно, но также и не то, что вы ищете.


Итак, давайте отступим назад и подумаем о том, каким вам нужно map() быть, чтобы это делало то, что вы хотите. При вызове вы arr.map() рассматриваете обратный вызов как универсальную функцию, которая преобразует входные данные универсального типа SomeType<V> в выходные данные типа OtherType<V> для any V . В противном случае нет никаких шансов заставить компилятор заметить корреляцию между каждым элементом входного кортежа и соответствующим элементом выходного кортежа. Вы действительно можете написать это при вызове map() , аннотируя обратный вызов:

 function convertValues<T extends any[]>(arr: [...SomeTypeList<T>]) {
  return arr.map(<V,>(
    { value }: SomeType<V>
  ): OtherType<V> => ({ otherValue: value }))
}
 

Проблема в том… как вы описываете такие общие обратные вызовы в целом, которые могут делать что-то еще? Я полагаю, вы не хотите жестко кодировать ввод map() , чтобы заботиться об обратных вызовах, которые конкретно превращаются SomeType<V> в OtherType<V> . Вы хотите сказать «обратные вызовы, которые превращаются F<X> в G<X> для любого F и G «:

 // not valid TypeScript, don't use it
interface Array<T> {
  map<A extends any[], F<?>, G<?>>(
    this: { [K in keyof A]: F<A[K]>},
    fn: <V>(v: F<V>) => G<V>
  ): { [K in keyof A]: G<A[K]> }
}
 

Но нет способа выразить это в TypeScript. F и G выше приведены не общие типы, а общие функции типов или конструкторы типов. И они не существуют в TypeScript. Конструкторы общих типов потребовали бы введения так называемых типов более высокого типа, которые можно найти в некоторых более функциональных языках программирования, таких как Haskell и Scala. Существует давний запрос open feature, microsoft / TypeScript # 1213, запрашивающий это, но кто знает, будет ли он когда-либо реализован. Это было бы довольно много работы, поэтому я не задерживаю дыхание (но хотел бы это увидеть!). и есть несколько возможных способов имитации типов с более высоким типом в TypeScript (подробнее об этом вы можете прочитать на GitHub), но я бы ничего не хотел рекомендовать для вашего варианта использования.

Итак, мы застряли. В настоящее время нет способа написать map() типизацию, чтобы заставить компилятор проверить, convertValues() соответствует ли реализация типу, который вы требуете вернуть.


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

Игровая площадка ссылка на код