Весь код этой статьи был протестирован с машинописным текстом 2.6.1

Компиляция TypeScript иногда может давать неожиданные результаты. Возьмем, например, следующий фрагмент кода:

Почему компилируется userList, а не user? Это не ошибка: это связано со структурной типизацией, на которую опирается система типов TypeScript парадигмы.

Что такое структурная типизация?

Большинство основных языков со статической типизацией (Java, C #, Scala и т. Д.) Используют систему типов, основанную на номинальной типизации -, что означает, что если два идентичных типа определены с разными именами, средство проверки типов вернет ошибку. если вы попытаетесь присвоить переменную первого типа переменной второго типа. Например в java:

Номинальная типизация делает связь между типами явной; подтип должен объявить свой родительский class или interface:

TypeScript использует структурную типизацию вместо номинальной типизации. Это означает, что компилятор не заботится о том, какое имя присвоено типу, и проверяет только совместимость структур типов. Предыдущий пример, написанный на TypeScript, будет выглядеть следующим образом:

В приведенном выше примере с точки зрения компилятора User и User2 - это одно и то же, это псевдонимы { name: string }. Типы полностью определяются своими атрибутами.

Важным следствием этого является то, что подтипы не должны объявлять свой родительский тип. Например, type UserWithAge = { name: string, age: number } является подтипом type User = { name: string }, потому чтоUserWithAge имеет все свойства User и дополнительные.

Поскольку UserWithAge является подтипом User, логично, что мы всегда можем присвоить переменную UserWithAge другой переменной User:

Тогда почему не будетconst user2: User = { name: 'Georges', age: 2 } компилироваться, хотя код явно правильный? В чем разница между этой строкой и двумя выше?

Пора ввести проверку лишних свойств.

Проверка лишнего имущества

Команда TypeScript выдвинула гипотезу о том, что при явном объявлении объекта определенного типа вам, вероятно, не нужны дополнительные свойства. По своему личному опыту я согласен с этим. Поэтому они добавили в компилятор специальную проверку для этого случая.

Он работает даже для полностью необязательных типов, так называемого слабого типа:

Итак, теперь мы понимаем, почему const user:User = { name:'Georges', age: 37 } не компилируется. Но тогда, ПОЧЕМУ const userList: Array<User> = [1,2,3].map((age) => ({name:'Georges', age: age})) проверка типа? Он должен быть таким же, мы явно объявляем объект типа User с дополнительным свойством, поэтому он не должен компилироваться, верно?

Это связано с тем, как работает вывод типов в TypeScript. В приведенном выше выражении TypeScript попытается определить, совместим ли результат выражения map с Array<User>. Для этого ему необходимо вычислить тип [1,2,3].map((age) => ({name:'Georges', age: age})), который сам зависит от типа функции (age) => ({name:'Georges', age: age}). [1,2,3] - это Array<number>, поэтому в приведенном выше обратном вызове age - это number. Тип возврата - {name:string, age:number}, а НЕ {name:string} AKA User.

Итак, тип [1,2,3].map((age) => ({name:'Georges', age: age})) проверяет нормально и возвращает Array<{name: string, age:number}>, совместимый с Array<User>.

Еще один интересный момент: даже написание [1,2,3].map<User>((age) => ({name:'Georges', age: age})) не изменит того, что внутренний обратный вызов набирается как (_:number) => {name:string, age:number}.

Итак, что мы можем сделать против такого поведения?

Что может быть сделано?

Как мы видели ранее, такое поведение возникает из-за того, как TypeScript пытается вывести типы. Способ обойти это - явно ввести выражение: const users: Array<User> = [1,2,3].map((age): User => ({name:'Georges', age: age})) тогда не удастся скомпилировать, потому что тогда TypeScript знает, что обратный вызов ДОЛЖЕН вернуть User.

Другой способ - использовать классы с частными свойствами, потому что TypeScript утверждает, что два частных свойства, даже структурно равные, никогда не могут быть совместимы:

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

На мой взгляд, два вышеупомянутых обходных пути слишком многословны, и я думаю, что лучший способ решить проблему - это… игнорировать ее. На самом деле это не проблема, если мы вернемся к нашему исходному коду:

Объект с дополнительным свойством может правильно ответить на любой вызов, который мы могли бы сделать для User, так что это действительный User, и нет реальной проблемы типа, связанной с наличием этого дополнительного свойства: мы можем игнорировать это безопасно.

Посмотрите на Flow

Flow - это типограф для JavaScript. Синтаксисы Flow и TypeScript очень похожи. Заметное различие между ними заключается в том, что в Flow по умолчанию отсутствует дополнительная проверка свойств.

Но Flow предлагает закрытые типы, то есть типы, которые не могут иметь дополнительных свойств:

Это также означает, что любой структурный подтип запечатанного типа НЕ совместим со своим родителем:

TypeScript, вероятно, было бы полезно реализовать что-то подобное.

Заключение

Понять систему структурных типов TypeScript не всегда легко, но в большинстве случаев, если TypeScript говорит, что что-то правильно, это потому, что это так - случаи несостоятельности существуют, но остаются редкими. Не спорьте с компилятором: позвольте ему вместо этого направлять вас в процессе разработки. Тем, кто интересуется системой типов TypeScript, я рекомендую прочитать: