- PVSM.RU - https://www.pvsm.ru -

ТайпСкрип: Ох уж эта весёлая система типов

Здравствуйте, меня зовут Дмитрий Карловский и недавно я, вместе с Артуром Мукминовым [1], проводил воркшоп, где показывал как разрабатывать сложные типофункции через тестирование [2]. Это 2 часа сурового программирования на типах. Так что в качестве тизера, ловите разбор курьёзов тайпскриптовой системы типов.

ТайпСкрип: Ох уж эта весёлая система типов - 1

Отношения — это сложно

Проверить является ли один тип подтипом другого очень просто, используя типотернарник:

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 пробивает дно во всех возможных смыслах. Тяжела участь тех, кто столкнётся с ним лицом к лицу...

ТайпСкрип: Ох уж эта весёлая система типов - 2

Весь код из статьи. [3]

Счастливой отладки, ребята!

Автор: Дмитрий Карловский

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/359411

Ссылки в тексте:

[1] Артуром Мукминовым: https://github.com/cevek

[2] как разрабатывать сложные типофункции через тестирование: https://github.com/nin-jin/tdtd

[3] Весь код из статьи.: https://www.typescriptlang.org/play?#code/C4TwDgpgBARg9nANhAhgOwPoEsDOGDyMAVhAMbBQC8UAgjjhAE7AA8AsAFBTewLLpQIAD2AQ0AExxRCJclAD8UYIwCu0AFxQAZikQMANJx5LVETgD5OnUJF5JUmXBjRxgGOMTIVqdBs3ZcPPD2AsKiElIeshSKympQmjp6EIaB3EkMFlYcNtAyXhgAbnhRXlS09EysRjwAwogo9FhaICzSnuT6UKVy5qnGANrt0V0A5JSUo109FAC6WRzW4NDB-I54cRDujBgZ0D6V-jXcqw79PJtQAD7aupkcloscYioAtlAAYgAyACxQAN60GA4FSMcRdD53FJQAAqpi6AFU0ABrFwAdzQUAAvtlcp9ftg8Co0Fg4JiDn5qml8T9ztwbt8fgA6GjA0Hia40pmQ5KcxlMuHxBm-JlI1FwDELJa2VkgsGE5xvGBMcq+KoBYz1Ro4ZqtLmy9ldNBKlV9Y5QIb8g1gsYTKZQY2vZWMKDzB645ZAuXiBVoCCFJgYNHALSqw5UzUNJotNpWtk2qAABigZupQz9AcYYxY6ntybdj2leT9vv9geDoYp6vNWujerj3q6AEYU3SLfr4+CoKN1OZ7RmVQXsi93t8AMwAr2Gz5QrqC6A4p5460+pwuNwrsOUjU8ACiAEcVLpY18xyzOxCRZvzJy9m29lLno6oAAJFBgMAgAAiEBgKgA5v+WBoP+ALmjyDDlAARPOUFdOa87QRBEBwZwi5FrCpgYGEYiSBgODKMBoFVkc1K1jqMavu+n4-n+gFEQK8JQARjBEa25pDG+H7fr+AFASBjFqNmuZdCxbFDkunrEuKGIKugIBbtW1LSeiaBtvJj54vJGCvCgIDKthIi4XgA4uiREY8PJghGREDpli6sSmAktzJG2lw3A+7pPAA9AAVP5AWBUFwUhaFoXeR6MrhgEACQNDkEeiCpDFu5CJA5AQByOERJwMU3AeiVtPFwCJV0qXpaIHKPHlUAALK4MAKDIhAbSjAA4q4UDEsIFWZUoyz2sViUphYqoJbokXQGqzAAHL2UV42INZ4SSHZmYpmNJUTZJtgFceUBfBAWjAF0ABKWD-gAFhQN7ULlAAUuUxSwAAa5j3QAlJQN73VAL3LcZB1HTEUAtpoABMUAfblH0A7Zj0cDFz1vZ931QL9-3Zat51XSDYNQJD0OI7DigqRKmKaKZ2TcJw4hkA0jDQKQZIEXVDVNRAADKIBOkgznElgh7QDgPPwIgk1swRHOxbVED0Cg-7QFjUhiSByUAJJoFocDlFTDzlLL8uK1AABkk5DPVUvNdzvNLbMmia9r2IS+RuptIdx1nRd10bdQHsUMrUA49d2QxYowcBzZq3+09ihDP7UC2pMXu466T2aPHwOJ92Ob2hHaeI7lmj54HMeI2H7b+2MvZ597czp5XWdjAAhMnQd166QA

[4] Источник: https://habr.com/ru/post/531030/?utm_source=habrahabr&utm_medium=rss&utm_campaign=531030