Я заметил интересную (возможно, хакерскую) часть в TypeScript lib.d.ts
:
interface PromiseConstructor {
/* ... */
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>;
/* ... */
}
Все кажется нормальным, кроме той части, где T extends readonly unknown[]
пишется как T extends readonly unknown[] | []
. Это выглядит странно, потому что []
расширяется readonly unknown[]
, поэтому readonly unknown[]
уже должно покрывать []
. Позже я понял, что это позволяет TS определить тип кортежа:
const f = <T extends readonly unknown[] | []>(x: T): T => x;
const test_0 = f([42, "foo"]);
// ^?: [number, string]
Без | []
это не работает:
const f = <T extends readonly unknown[]>(x: T): T => x;
const test_0 = f([42, "foo"]);
// ^?: (number | string)[]
Соответствующий коммит GitHub, который я нашел, — вот этот.
Механизм, стоящий за этим, кажется весьма волшебным, и мне любопытно, сможет ли кто-нибудь объяснить особенности его работы.
const
, но существующая вещь не сломана, поэтому ее не исправляют. Это полностью решает вопрос? Если да, то напишу ответ или найду подходящий источник дублирования. Если нет, то что мне не хватает?
@jcalz Это хорошее объяснение. Было бы здорово, если бы вы написали ответ. Спасибо.
TypeScript использует различные эвристические правила, чтобы определить, как вывести подходящий тип значения, поскольку множество типов совместимы с любым заданным значением. Например, значение [42, "foo"]
присваивается типам: (number | string)[]
; readonly unknown[]
; (42 | "foo")[]
; [42, string, ...Date[]]
; unknown
и многие другие. Как TypeScript выбирает?
Один из способов принятия решения — посмотреть контекст, в котором появляется значение; этот контекст может «ожидать» определенного диапазона типов, который влияет на вывод. Без контекста [42, "foo"]
понимается как (string | number)[]
, что является хорошим выбором в широком диапазоне случаев использования.
Но если [42, "foo"]
появляется в контексте, который ожидает типы кортежей , то, скорее всего, будет выведен тип кортежа, подобный [string, number]
(см. этот комментарий к microsoft/TypeScript#27179).
Параметр ограниченного типа , например T extends U
, заставляет U
фигурировать в контексте для вывода T
. Итак, если T extends readonly unknown[]
, то в контексте нет типа кортежа и [42, "foo"]
выводится как (string | number)[]
. Но если T extends readonly unknown[] | []
, то теперь в области T
явно указан тип кортежа (хотя да, объединение[]
и readonly unknown[]
не содержит больше значений, чем просто readonly unknown[]
), и, таким образом, выводится [number, string]
.
Обратите внимание, что варианты кортежей также служат контекстом кортежа, поэтому вы также можете написать
const f = <T extends unknown[]>(x: readonly [...T]) => x;
const test_0 = f([42, "foo"]);
// ^?: const test_0: readonly [number, string]
Предположительно, если бы типизация TypeScript для Promise.all была написана сегодня, она использовала бы вариационные кортежи вместо пустых типов кортежей. Или, может быть, он даже будет использовать параметр константного типа, чтобы явно запрашивать еще более узкие типы:
const f = <const T extends readonly unknown[]>(x: T) => x;
const test_0 = f([42, "foo"]);
// ^?: const test_0: readonly [42, "foo"]
Я не уверен, чем это будет отличаться, но
<const T ... >(...)
, кажется, также позволяет вывести кортеж , что кажется гораздо менее волшебным. Я предполагаю, что этот тип злоупотребил особенностью реализации до того, как были добавлены дженерики, и никогда не обновлялся. Хотя я уверен, что другие здесь дадут гораздо более глубокий ответ.