Весь код этой статьи был протестирован с машинописным текстом 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.