Это перевод, но перевод моей собственной статьи, так что не спешите убегать на «неповторимый оригинал». Я пишу на тайпскрипте уже довольно давно. Но некоторые вопросы все еще сбивают меня с толку: Если мне нужен объект, который реализует и { name: string }, и { age: number }, нужно эти типы & (пересечь) или | (объединить)? В каждом варианте можно найти логику, потому что я хочу левое и правое, но, с другой стороны, мне нужно объединение интерфейсов. Сработает ли type S<T> = T extends string ? ..., если T — юнион строк, вроде 'ru' | 'de'? В чем разница межу any и unknown? Лучшее предложение интернета — дурацкие мнемоники типа «Avoid Any, Use Unknown». А что не так с any? never — что за тип? «never по-английски значит НИКОГДА, и это значение НИКОГДА не появится в программе» звучит очень драматично, но не сильно помогает. Если never это какой-то взрыв, то почему я могу const x: number = y as never? И почему never всегда  extends X? const x: {} = true; — правильно типизированный код. Ну как это вообще, а? true точно не пустой объект. Если вам легко ответить на все эти вопросы — вы молодец. Правда. Здорово, что в мире есть такие умные люди. Я вот не мог, и решил это исправить. Пока я разбирался с never, мне попалась отличная статья (да и весь этот блог очень рекомендую), в которой была одна особо интересная мысль: на самом деле never — пустое множество значений. Если впустить в свою душу идею, что тип — это просто множество значений, всё встает на свои места. Я ушел в пещеру, разобрал все свои знания о тайпскрипте, а потом собрал их на место по чертежу из теории множеств, и получилось логично. Давайте сделаем это вместе: Освежим наши знания о теории множеств. Посмотрим, как понятия TS соотносятся с множествами и операциями на них. Для разминки переведем на язык множеств булевы типы (а заодно — null и undefined). Обобщим это на числа (и походу выясним, какие типы TS вообще не может выразить). Перейдем к интерфейсам — оказывается, они работают совсем не так, как я думал! И на десерт — разложим по полочкам any и undefined. В конце я нахожу ответы на все свои вопросы, выстраиваю TS в стройную теорию, и рисую эту великолепную диаграмму: Но для начала освежим в памяти теорию множеств. Если вы и так все знаете, листайте дальше, но я кончал университеты давно и хотя бы для себя распишу, что к чему. Множество — неупорядоченная коллекция элементов. На детсадовском примере: у нас есть два яблока — это наши элементы. Чтобы не путаться, назовем их яблоко вася и яблоко петя. Еще у нас есть пакетики, в которые яблоки можно класть — это множества. Всего есть четыре способа набрать яблок в пакет: Пакет с яблоком васей, { вася } — множества пишут как элементы в фигурных скобках. Пакет с яблоком петей, { петя }, ничего нового. Пакет с двумя яблоками, { вася, петя }. В каком порядке мы их туда клали — совершенно неважно. Не хочу вас пугать, но такое множество называют универсом, потому что сейчас в нашей модели мира нет ничего кроме этих двух яблок. Еще можно вообще ничего не класть в пакет, получится пустое множество. Для него есть особый символ ∅ Множества часто изображают на диаграммах Венна — как будто все элементы разложены на плоскости, и мы обводим их кружочками: Вместо того, чтобы перечислять все элементы, множество можно определить условием. Например, «R — множество красных яблок» это R = { вася } (если вася — красный, а петя ещё зелёный). Множество A называют подмножеством B, если все элементы A входят в B. В нашем яблочном мире { вася } - подмножество { вася, петя }, но { петя } — не подмножество { вася }. Обратите внимание: Любое множество — подмножество самого себя. Любое множество — подмножество универсального множества. Пустое множество — подмножество любого множества. Несколько полезных операций с множествами: Объединение C = A ∪ B — все элементы, которые входят хотя бы в A или в B (свалили два пакета в один). Конечно же, A ∪ ∅ = A Пересечение C = A ∩ B — все элементы из A, которые входят еще и в B. Логично, что A ∩ ∅ = ∅ Разность C = A \ B — все элементы из A, которых нет в B. Без сомнений, A \ ∅ = A Всё, этого должно хватить, чтобы разобраться в тайпскрипте. Посмотрим, как применить эти понятия к типам. Итак, невероятный поворот: в принципе, тип — множество JavaScript значений. Подробнее: Универсальное множество — вообще все значения, которые могут появиться в JS-программе. Тип (даже не TS-тип, просто тип) — какое-то множество JS-значений. TS может описать некоторые типы, а некоторые — не может. Не верите? Попробуйте написать тип «все числа, кроме 0». A extends B из условных типов и констрейнтов можно читать как «A — подмножество B». TS-операторы | и & — как раз объединение и пересечение типов как множеств. Exclude<A, B> по идее моделирует разность множеств, но этот джинерик работает не для всех A и B (вспоминаем пример с числом-кроме-0, Exclude<number, 0> не работает). never — пустое множество. Доказательство: для любого A A & never = never b A | never = A, а Exclude<0, 0> = never. Понимаю, что сложно сразу это принять, так что попробуем на примере. Сделаем вид, что в JS есть только булевы значения (я не хотел бы писать на этом). Таких значений ровно два: true и false, или, как говорил наш препод, трюэ и фалзё. Это те же яблочки, только в профиль. На булевых значениях можно составить 4 типа: Типы-литералы true и false, в каждом — по одному значению. boolean, тип из обоих булевых значений. И never в роли пустого множества. Диаграмма получится та же, что и для яблок: Поупражняемся в телепортации из мира множеств в мир типов: boolean — то же, что true | false (на удивление, именно так этот тип и реализован в TS) true — подмножество (или подтип) boolean never — пустое множество, значит, never — подмножество типов true, false и boolean & — пересечение, значит, false & true = never, boolean & true = { true, false } ∩ { true } = true (то есть универсальный boolean не влияет на пересечение), true &amp; never = never и так далее. | — объединение, значит, true | never = true, а boolean | true = boolean (то есть универсальный boolean «проглатывает» все остальные элементы объединения, потому что они уже являются его подмножествами). И даже Exclude правильно вычисляет разность множеств: Exclude<boolean, true> = false (в общем случае для других типов это не так). Теперь потренируемся на extends-условиях: Если вспомнить, что extends можно читать как «является подмножеством», ответить легко — A0,B1,C1,D1. Хотя интуитивно сложно понять, как never может что-то экстендить. Это успех. Типы null и undefined устроены так же, как и boolean, но в каждом из них всего по одному значению (или по два TS-типа с учетом never). null & boolean = null & undefined = boolean & undefined = never, потому что одно значение никак не может быть сразу двух JS-типов (то есть базовые JS-типы — непересекающиеся множества). Нанесем всё это на нашу карту: Окей, с простыми типами разобрались, перейдем к строкам. На первый взгляд кажется, что тут всё то же самое: string — тип всех JS-строк, а у каждой конкретной строки есть свой литерал-тип: const str: 'hi' = 'hi'. Но есть один маленький нюанс — строк, в отличие от булевых значений, бесконечно много. (В память компьютера влезет только конечное количество строк? Не душните, их достаточно, чтобы перечислять все было непрактично. К тому же, системе типов негоже ограничивать себя грязным реальным миром). Как и множества вообще, строковые типы в TS можно определять несколькими способами: Через объединение | можно задать любое конечное множество (тип) строк — например, type Country = 'de' | 'us'. А вот бесконечное (например, все строки длиннее двух символов) — нельзя, потому что написать бесконечный список элементов довольно проблематично. (Относительно) свежая фича TS — шаблонные строковые типы — умеет определять некоторые бесконечные множества — например, type V = `v${string}` — множество всех строк, которые начинаются с v Мы сможем наковырять ещё несколько типов, объединяя и пересекая шаблоны и литералы. TS достаточно крут, чтобы смержить шаблон и объединение литералов: 'a' | 'b' & `a${string}` = 'a'. Ещё TS старается смержить пересечение шаблонов, но получается не всегда: `a${string}` & `b${string}` — очень извращённая запись never, потому что строка не может одновременно начинаться и с a, и с b. Но как бы мы ни старались, некоторые строковые типы описать в TS не выйдет. Из простого — попробуйте придумать тип для любой строки, кроме 'a'. На ум приходит Exclude<string, 'a'>, но, посколько TS не моделирует тип string как объединение всех возможных литералов, это не сработает и в результате мы получим снова string. Шаблоны тоже не могут выразить этот тип. Типы чисел, символов и бигинтов работают так же, но там даже нет шаблонов, так что мы ограничены конечными множествами. А мне бы пригодились типы «целое число», «число от 0 до 1» или «положительное число». Ну да ладно, всё вместе: Уф, примитивы обсудили! Надеюсь, мы научились переходить с языка типов на язык множеств и обратно. Заодно мы убедились, что вовсе не все типы можно записать на TS. Теперь — самое сложное. Если вы совершенно уверены, что const x: {} = 9 — баг TS, сейчас мы вместе убедимся что это не так. Оказывается, в этом есть логика, просто наше представление о TS-объектах (они же интерфейсы, они же Record) построено на неправильных предпосылках. Во первых, по аналогии с примитивными типами логично предположить, что type Sum9 = { sum: 9 } — тип для объекта-литерала, в который влезет только объект { sum: 9 }. Так вот, это работает совсем наоборот. Тип Sum9 стоит читать как «штучка, у которой по ключу sum можно достать число 9». То есть каждый тип поля в интерфейсе — условие, которое отсекает что-то от множества «штук». И обычно такой подход довольно полезен — ведь все любят пихать в функцию (data: Sum9) => number объекты с дополнительными свойствами вроде obj = { sum: 9, date: '2022-09-13' } без ругани от TS. Значит, и type O = {} — не тип «пустой объект» для литерала {}, а «штучка, у которой можно получать доступ к свойствам, но в целом свойства мне не нужны». Становится понятнее, как работает наш «баг»: если x = 9, то штучка x удолетворяет нашему описанию в интерфейсе {}. Спасибо автобоксингу, можно делать даже более смелые утверждения вроде const x: { toString(): string } = 9 — мы же можем вызвать x.toString() и получить строку? Можем. Все честно. А вот null и undefined в наш интерфейс не влезут, потому что у них принципиально нельзя получить никакое свойство. Не могу сказать, что это супер-интуитивно, но теперь по крайней мере логично. Если помните, я путаю | и &. Так вот, эти операторы действуют на типы как на множества объектов, а не на «форму объектов» или «множества свойств». Если мне нужны объекты, у которых есть и name, и age, то нужно использовать объединение — { name: string } & { age: number }. А что насчет типа object? Поскольку каждое свойство в интерфейсе отрезает какую-то часть значений от множества почти-всех-значений, у нас не выйдет аккуратно убрать все примитивы. И поэтому в TS есть специальный базовый тип, который как раз и обозначает «JS-объект, а не примитив». Конечно, интерфейс можно пересекать с типом object, чтобы получить «JS-объекты с нужными свойствами, но не примитивы» — например, object &amp; { toString(): string } не содержит число 9. Добавим эти типы на нашу схему (почти закончили): Для последнего рывка нужно хорошенько разобраться с extends. Это слово из ООП, где тип расширяет своего родителя в смысле добавления новой функциональности, а с точки зрения множеств оно скорее путает нас — ведь расширенное множество в геометрическом смысле должно быть больше исходного. Я предлагаю не зацикливаться на этом и не представлять цепочки наследования, которых тут нет. Просто читайте A extends B как «A является подмножеством B». На примерах: 0 | 1 extends 0 — ложь, потому что {0, 1} — не подмножество {0} (даже хотя {0,1} расширяет {1} в геометрическом смысле). never extends T всегда правда, потому что пустое множество never — подмножество любого другого множества. Какого-то здравого смысла тут нет, просто так работает модель. T extends never выполняется только для T = never, потому что у пустого множества нет подмножеств кроме себя. В T extends string без проблем влезут и литерал, и шаблон, и любое их объединение, и сам string, потому что все они — подмножества string. А вот T extends string ? string extends T ? проверяет, что T точно совпадает с типом string, потому что только string является одновременно и подмножеством, и надмножеством string. В TS не один, а целых два типа, которые моделируют произвольное JS-значение: unknown and any. В чём разница? unknown хорошо ложится в наше объяснение с множествами. Это универсальное множество всех JS-значений, без каких-то конкретных обещаний. Тут есть и null, и undefined, и любой объект, и число: Хотя есть и странность.  unknown не реализован как объединение всех базовых типов, так что сделать Exclude<unknown, string> не выйдет.  unknown extends string | number | boolean | object | bigint | symbol | null | undefined не выполняется, из чего теоретически следует что в JS бывают ещё какие-то другие значения (это не так). Ну, что делать, деталь реализации. А вот any с точки зрения типов-множеств ведет себя странно: any extends string ? 1 : 0 возвращает 0 | 1, то есть «не знаю». И даже any extends never ? 1 : 0 возвращает 0 | 1, то есть any может быть и пустым множеством. Из этого можно было бы заключить, что any — «какое-то множество, но мы не знаем, какое» — вроде NaN в мире типов. Но эта гипотеза ломается о то, что на вопросы string extends any, unknown extends any и даже any extends any TS уверенно отвечает «да» вместо «не знаю». Так что any — парадокс множеств, и анализировать его с этой точки зрения бессмысленно.Единственная хорошая новость — any extends unknown, так что в any не входит никаких чудо-значений, и unknown — все еще все JS-значения. Закончим нашу великолепную карту типов, завернув её в unknown, и добавим any в роли перста Божьего: Сегодня мы узнали, что типы TS — просто множества JS-значений. Вот небольшой множество-типовой разговорник: unknown — универсальное множество (все JS-значения) never — пустое множество A extends B — А является подмножеством B | — объединение множеств, & — пересечение Exclude — непереводимый фольклор, примерно соответствующий разности множеств. С этими новыми знаниями вернемся к моим вопросам: & и | работают только на множествах значений, а не на «форме объектов». Если я хочу объект, который удовлетворяет сразу двум интерфейсам, их надо пересечь. type <T> = T extends string ? ... сработает и для T = 'string', и для T = 'a' | 'b', потому что все эти типы — подмножества string unknown — хорошая модель «множества всех JS-значений». any просто отключает проверку типов и ведет себя нелогично. never — не НИКОГДА, а пустое множество. Раз extends читается как «является подмножеством», то вполне логично и то, что объединение с ним не меняет исходное множество, и что never extends что угодно (ведь все 0 элементов пустого множества содержатся в чём угодно) {} — не особый тип, в который влезает только пустой объект, а «штука без ограничений по типам свойств», так что число 9 вполне подходит.  На сегодня всё! Если вам было интересно, подписывайтесь на мой канал в телеграме.  Фронтендер