IsUnion
Проблема
Реализовать тип IsUnion
, который принимает тип параметр T
и возвращает
true
, если T
это объединение типов. Например:
type case1 = IsUnion<string>; // false
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false
Решение
Когда я увидел эту проблему, не знал с чего даже начать. Потому что нету решения в TypeScript, которое можно использовать для реализации такого типа. Нету никаких встроенных типов, которые бы хоть как-то помогли.
Поэтому, самое время включать креативное мышление и использовать подручные средства. Начнём с того, что подумаем об объединениях типов и как они представлены.
Когда вы указываете плоский тип, например string
, то значениями этого типа не
будет ничего другого, только string
. Но, если вы указываете объединение типов,
например string | number
, то получаете возможные значения как string
, так и
number
.
Плоские типы представляют только один набор возможных значений, а объединения представляют набор из наборов возможных значений. И нет никакого смысла в дистрибутивной итерации для плоских типов, но смысл есть для объединений типов.
И эту ключевую точку мы и возьмем за фактор, как можно определить объединение
типов. Когда мы дистрибутивно перебираем по типу T
, который не является
объединением, это ничего не меняет. Но, это меняет многое для объединений типов.
У TypeScript есть одна возможность языка - дистрибутивные условные типы. Когда
вы пишете конструкцию T extends string ? true : false
, где T
это
объединение, TypeScript применит условный тип к каждому элементу из объединения
дистрибутивно. Грубо говоря, это будет выглядеть вот так.
type IsString<T> = T extends string ? true : false;
// Например, передаем параметр T = string | number
// Дистрибутивное применение условного типа будет выглядеть как-то так
type IsStringDistributive = string extends string
? true
: false | number extends string
? true
: false;
Видите к чему я хочу подвести? Если тип T
это объединение, используя
дистрибутивные условные типы можно разбить его и сравнить с входным тип
параметром T
. В случае, если они одинаковые - это не было объединением. Но,
если они не будут одинаковыми, значит это объединение. Потому что string
не
сравнится с string | number
и точно так же number
не сравнится с
string | number
.
Давайте уже начнём с реализации! Сначала, сделаем копию входного параметра T
,
чтобы сравнивать с изначальным объединением без изменений.
type IsUnion<T, C = T> = never;
Применяя условные типы, получаем дистрибутивную семантику. Внутри правдивой ветки условного типа, получим каждый элемент из объединения по отдельности.
type IsUnion<T, C = T> = T extends C ? never : never;
Теперь, важная часть - сравнить элемент из объединения с изначальным входным тип
параметром T
. В случае, если эти типы будут одинаковыми, это значит что в
обеих случаях было по одному элементу - не объединение. Иначе, дистрибутивные
условные типы сделали дело и мы сравниваем один элемент из объединения с
изначальным объединением - значит это объединение.
type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;
Готово! Чтобы прояснить момент сравнения, посмотрим, какие типы держат в себе
[C]
и [T]
внутри дистрибутивного условного типа.
Когда мы передаем тип, не являющийся объединением, например string
, они держат
одинаковые типы. Следовательно, это не объединение, возвращаем false
.
[T] = [string][C] = [string];
Но, если передаём объединение, например string | number
, они держат разные
типы. Наша копия C
держит в себе кортеж с нашим изначальным объединением, а
T
держит в себе объединение из кортежей. Следовательно, типы разные, считаем
это объединением и возвращаем true
.
[T] = [string] | [number]
[C] = [string | number]
Комментарии