Весь код этой статьи был протестирован с машинописным текстом 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, я рекомендую прочитать:
- документация, разумеется, и в частности страница расширенных типов;
- удивительно богатый информацией FAQ.