Здравствуйте, меня зовут Дмитрий Карловский и недавно я, вместе с Артуром Мукминовым, проводил воркшоп, где показывал как разрабатывать сложные типофункции через тестирование. Это 2 часа сурового программирования на типах. Так что в качестве тизера, ловите разбор курьёзов тайпскриптовой системы типов.
Отношения это сложно
Проверить является ли один тип подтипом другого очень просто, используя типотернарник:
type IsAExtendsB = A extends B ? true : false
На воркшопе мы разработали типофункцию Classify, принимающую 2 типа и возвращающую одно из 4 возможных значений:
[ A, '<:', B ]
A является строгим подтипом B.[ A, ':>', B ]
B является строгим подтипом A.[ A, '==', B ]
Оба типа являются подтипами друг друга (но не обязательно являются одинаковыми типами).[ A, '!=', B ]
Ни один тип не является подтипом другого.
Кроме того, мы запилили типофункции Equal и Assert, позволяющие сравнивать типы на равенство, независимо от того, считает ли компилятор два разных типа подтипами друг друга или нет. Assert при этом ещё и валит проверку типов, если типы вдруг не совпали.
Всё есть объекты! Но это не точно..
Ну и первый же прикол Object
и object
это определённо разные типы, ибо примитивные типы являются
подтипами первого, но не второго:
type boolean_is_Object = Assert< boolean extends Object ? true : false, true>type boolean_is_not_object = Assert< boolean extends object ? true : false, false>
Однако, если мы сравним их, то выяснится, что они являются подтипами друг друга:
type Object_vs_object = Assert< Classify< Object, object >, [ Object, '==', object ]>
То есть отношение подтипизации в тайпскрипте не является
транзитивным: если один тип (например, boolean
)
является подтипом другого (например, Object
), а другой
третьего (например, object
), то первый вовсе не
обязательно является подтипом третьего это надо проверять
отдельно.
На диаграмме, все объектные типы раскрашены в голубой цвет. Они
являются подтипами как Object
, так и
object
.
Разные типы перечислений типов
Раз уж мы заговорили про булевый тип, то нельзя не упомянуть, что он ничто иное, как кроткий алиас для объединения пары литеральных типов:
type boolean_is_true_or_false = Assert< boolean, true | false>
С числовыми перечислениями всё в принципе аналогично:
enum FL4 { Absurd, False, True, Unknown }type FL4_is_union = Assert< FL4, | FL4.Absurd | FL4.False | FL4.True | FL4.Unknown>
И состоят они вроде бы из чисел (даже не литералов):
type Absurd_is_number = Assert< Classify< FL4.Absurd, number >, [ FL4.Absurd, '==', number ]>
Но тут тайпскрипту внезапно сносит крышу:
type Absurd_is_never_wtf = Assert< Classify< FL4.Absurd, 0 >, [ never, '<:', 0 ]>
Эй, тайпскрипт, ты куда первый тип потерял? Верни, где взял!
type One_is_never_wtf = Assert< Classify< FL4.Absurd, 1 >, [ FL4.Absurd, ':>', never ]>
Вот, спасибо, совсем другое дело!
По всей видимости связано это с тем, что значения перечислений это не не простые литералы, а уникальные:
enum FL3 { Absurd, False, True }type Absurd_is_not_Absurd = Assert< Equal< FL3.Absurd, FL4.Absurd > | false, false>
Ну да ладно, у нас ещё остались не разобранными строковые перечисления. Может показаться, что ведут они себя как и числовые, однако, внезапно:
enum HappyDebugging { False = "True", True = "False",}type True_extends_string = Assert< Classify< HappyDebugging.True, string >, [ HappyDebugging.True, '<:', string ]>
Получается, что number
является подтипом числового
перечисления, а вот string
подтипом строкового уже
нет.
Призраки прошлого
В Тайпскрипте есть пара специальных типов, которые находятся на противоположных концах иерархии:
never
представляет из себя пустое множество значений. То есть он является подтипом любого типа, и никакой другой тип не может быть его подтипом.unknown
же это множество всех возможных значений. То есть это объединение вообще всех типов в один. Поэтому любой тип является подтипомunknown
.
Но что это маячит рядом с ними? Да это же
any
! С одной стороны он полностью взаимозаменяем
с unknown
:
type unknown_is_any = Assert< unknown, any>
Но с другой же, он как кот Шрёдингера является подтипом
never
(и как следствие, любого другого типа до
unknown
) и не является таковым одновременно:
type any_maybe_extends_never = Assert< any extends never ? true : false, true | false>
Короче, any
пробивает дно во всех возможных
смыслах. Тяжела участь тех, кто столкнётся с ним лицом к
лицу...
Счастливой отладки, ребята!