Дженерики
Параметры типа для переиспользуемого типобезопасного кода, ограничения дженериков через extends и стандартные служебные типы на их основе.
12 вопросов
JuniorТеорияОчень частоЧто такое дженерики в TypeScript и зачем они нужны?
Что такое дженерики в TypeScript и зачем они нужны?
Дженерики параметризуют функцию, класс или тип переменной типа, поэтому один и тот же код работает с разными типами, сохраняя точный тип. function identity<T>(x: T): T возвращает тот тип, который получил, поэтому identity(5) имеет тип number. В отличие от any, который стирает тип, дженерик сохраняет связь между входом и выходом, оставаясь полностью типобезопасным.
Типичные ошибки
- ✗Тянуться к
anyвместо параметра типа, теряя связь типа входа и выхода - ✗Думать, что дженерики существуют в рантайме — они стираются, как и все типы
- ✗Писать отдельную перегрузку на каждый тип, когда один дженерик покрыл бы все
Уточняющие вопросы
- →Как дженерик сохраняет информацию о типе, которую
anyотбросил бы? - →Может ли компилятор вывести аргумент типа, или его всегда нужно передавать?
JuniorТеорияОчень частоЧто дают Partial<T>, Required<T> и Readonly<T>?
Что дают Partial<T>, Required<T> и Readonly<T>?
Каждый — mapped-тип поверх keyof T, меняющий один модификатор. Partial<T> делает все свойства необязательными, добавляя ?, Required<T> снимает необязательность через -?, а Readonly<T> помечает каждое свойство как readonly. Все три поверхностные и стираются из итогового JavaScript.
Типичные ошибки
- ✗Ожидать от них рекурсии — все три поверхностные, только верхний уровень
- ✗Думать, что
Readonly<T>замораживает объект в рантайме, какObject.freeze - ✗Забывать про
Required<T>и писать mapped-тип с-?руками
Уточняющие вопросы
- →Как написать глубокий
Readonly, который всё же спускается во вложенные объекты? - →Какой модификатор снимает
-?и для чего нужна форма+?
JuniorТеорияЧастоКак TypeScript выбирает T, когда дженерик вызывают без аргументов типа?
Как TypeScript выбирает T, когда дженерик вызывают без аргументов типа?
Компилятор выводит T из аргументов вызова: он сопоставляет тип каждого аргумента с параметром, где упомянут T, и выбирает лучшего общего кандидата, поэтому identity(5) даёт T = number. Если T нет ни в одном параметре, он откатывается к значению по умолчанию или к unknown.
Типичные ошибки
- ✗Считать, что пропущенный аргумент типа становится
any, а не выводится - ✗Ожидать, что вывод пойдёт обратным ходом от аннотации принимающей переменной
- ✗Забывать, что у параметра, стоящего только в типе возврата, нет источника вывода
Уточняющие вопросы
- →Во что выводится
T, если он стоит только в типе возврата функции? - →Как ограничение на
Tвлияет на то, что компилятор выводит из вызова?
MiddleКодЧастоТипизируйте обёртку memoize, сохраняющую аргументы и результат функции
Типизируйте обёртку memoize, сохраняющую аргументы и результат функции
Обобщайтесь по типу функции и пересоберите из него сигнатуру: (...args: Parameters<T>) => ReturnType<T>. Parameters<T> достаёт кортеж аргументов, ReturnType<T> — результат, поэтому кэширующая версия проверяется ровно как исходная.
Типичные ошибки
- ✗Возвращать
(...args: any[]) => any, теряя все проверки, что были у исходной функции - ✗Типизировать обёрнутую функцию как
Function— тогдаParameters<T>к ней не применить - ✗Забывать, что ключ кэша строится из аргументов, а не из
T
Уточняющие вопросы
- →Как
ReturnType<T>достаёт результат черезinfer? - →Что сломается, если обёрнутая функция перегружена?
MiddleТеорияЧастоЧто сохраняет обобщённый параметр из того, что any выбрасывает?
Что сохраняет обобщённый параметр из того, что any выбрасывает?
Обобщение сохраняет связь между входом и выходом: в first<T>(xs: T[]): T компилятор связывает T в месте вызова и возвращает тот же тип. first(users) даёт User. С any[] связь разорвана: результат — any, и любое обращение к его свойствам не проверяется.
Типичные ошибки
- ✗Считать
<T>документацией, которую компилятор заменяет наany - ✗Аннотировать помощник как
any[], а потом удивляться, что результат не проверяется - ✗Ждать, что
Tможно будет прочитать во время выполнения после вызова
Уточняющие вопросы
- →Чем
unknownотличается здесь отanyв роли типа параметра? - →Что даёт внутри тела добавленное ограничение
T extends { id: string }?
MiddleКодЧастоТипизируйте помощник, принимающий один элемент или массив и возвращающий массив
Типизируйте помощник, принимающий один элемент или массив и возвращающий массив
Объявите function toArray<T>(input: T | T[]): T[]. Компилятор выводит T из любой ветви: и toArray(user), и toArray(users) дают User[]. Внутри Array.isArray(input) сужает объединение, а аргумент типа не нужен.
Типичные ошибки
- ✗Считать, что вывод не может разрешить
Tчерез параметрT | T[] - ✗Возвращать
T[] | T[][], не сузив объединение внутри тела - ✗Браться за перегрузки там, где хватает одного обобщённого union-параметра
Уточняющие вопросы
- →Что выведется для
TприtoArray(null)и как это исключить? - →Как изменится сигнатура, если на вход может прийти
readonly T[]?
MiddleТеорияЧастоКак ограничить параметр типа дженерика в TypeScript?
Как ограничить параметр типа дженерика в TypeScript?
Используйте extends, чтобы потребовать совместимости аргумента типа с заданной формой: function len<T extends { length: number }>(x: T) принимает только типы, у которых есть length, поэтому x.length разрешён внутри тела. Ограничение сужает то, что можно передать, сохраняя точный тип. Можно ограничить один параметр другим, например <K extends keyof T>, чтобы безопасно индексировать T.
Типичные ошибки
- ✗Использовать
implementsвместоextendsдля выражения ограничения дженерика - ✗Забыть ограничение и потом не мочь обратиться к членам
T - ✗Думать, что
extendsздесь прикалываетTк одному типу, а не задаёт верхнюю границу
Уточняющие вопросы
- →Как
<K extends keyof T>делает индексированный доступ типобезопасным? - →Может ли ограниченный параметр всё ещё выводиться из аргументов вызова?
MiddleТеорияЧастоЧто делают служебные типы Pick, Omit и Exclude?
Что делают служебные типы Pick, Omit и Exclude?
Pick<T, K> строит тип только с ключами K из T. Omit<T, K> строит тип со всеми ключами T, кроме K — это буквально Pick<T, Exclude<keyof T, K>>. Exclude<U, E> работает с объединением, убирая из U члены, совместимые с E. То есть Pick и Omit выбирают или отбрасывают свойства объекта, а Exclude фильтрует члены объединения.
Типичные ошибки
- ✗Путать
PickиOmit—Pickоставляет перечисленные ключи,Omitих отбрасывает - ✗Применять
Excludeк ключам объекта вместо членов объединения - ✗Думать, что служебные типы работают в рантайме, а не на этапе компиляции
Уточняющие вопросы
- →Как
Omit<T, K>выражается черезPickиExclude? - →Какой служебный тип вы бы использовали, чтобы сделать все свойства необязательными?
MiddleДизайнИногдаВы ревьюите слой доступа к данным. Часть обобщённых помощников вызывают с явным аргументом типа почти в каждом месте вызова — fetchOne<User>('/users/1'), parseBody<Order>(req), — а другим его не задают никогда и всю работу делает вывод типов. Коллега просит правило для команды. Объясните, что вообще делает параметр типа выводимым, что делает компилятор, когда параметр типа не встречается ни в одной позиции параметра, и почему явно переданный аргумент типа у такого помощника — утверждение иного рода, чем выведенный компилятором. Скажите, где уместен каждый стиль.
Вы ревьюите слой доступа к данным. Часть обобщённых помощников вызывают с явным аргументом типа почти в каждом месте вызова — fetchOne<User>('/users/1'), parseBody<Order>(req), — а другим его не задают никогда и всю работу делает вывод типов. Коллега просит правило для команды. Объясните, что вообще делает параметр типа выводимым, что делает компилятор, когда параметр типа не встречается ни в одной позиции параметра, и почему явно переданный аргумент типа у такого помощника — утверждение иного рода, чем выведенный компилятором. Скажите, где уместен каждый стиль.
Параметр выводим лишь там, где он встречается в позиции параметра. У fetchOne<User>(url) в аргументах нет User, поэтому аргумент типа — непроверенное утверждение об ответе; там место схеме времени выполнения. Дайте выводу работать всюду, где тип несёт аргумент.
Типичные ошибки
- ✗Читать явный
<User>у fetch-помощника как проверку, а не как утверждение - ✗Ждать выводимости от параметра типа, которого нет ни в одном аргументе
- ✗Писать аргумент типа в каждом вызове даже там, где тип уже несёт сам аргумент
Уточняющие вопросы
- →Что выведет компилятор, если параметр типа не встречается ни в одном параметре?
- →Как валидатор схемы времени выполнения превращает это утверждение в реальную проверку?
MiddleТеорияИногдаЧто объявляют модификаторы in и out у параметра типа?
Что объявляют модификаторы in и out у параметра типа?
Они объявляют вариантность, которую компилятор иначе вывел бы сам: out T — T встречается только в выходных позициях (ковариантно), in T — только во входных (контрвариантно), in out T инвариантен. Аннотацию сверяют с реальным использованием, расхождение — ошибка.
Типичные ошибки
- ✗Читать
in/outкак ограничение на то, где можно передать аргумент типа - ✗Считать, что аннотации верят, а не сверяют её с реальным использованием
- ✗Ждать от модификаторов какого-либо эффекта во время выполнения
Уточняющие вопросы
- →Какую ошибку вы получите, если
out Tиспользован во входной позиции? - →Как компилятор определял вариантность до появления этих модификаторов?
MiddleТеорияИногдаПочему (a: Animal) => void присваивается в (d: Dog) => void, а наоборот — нет?
Почему (a: Animal) => void присваивается в (d: Dog) => void, а наоборот — нет?
Параметры функций контрвариантны: обработчик любого Animal справится и с тем Dog, который придёт на деле, поэтому более широкий параметр безопасен. Обработчик только для Dog сломался бы на Cat. strictFunctionTypes включает это для свойств функционального типа.
Типичные ошибки
- ✗Считать, что параметры следуют тому же направлению, что и отношение подтипа на значениях
- ✗Верить, что
strictFunctionTypesделает контрвариантными и параметры методов - ✗Думать, что результат
voidделает проверку параметров несущественной
Уточняющие вопросы
- →Почему та же проверка остаётся бивариантной для метода в краткой записи?
- →Как проверяется тип элемента массива и почему это несостоятельно?
SeniorТеорияРедкоПочему параметры методов проверяются бивариантно, а параметры свойств-функций — нет?
Почему параметры методов проверяются бивариантно, а параметры свойств-функций — нет?
Это намеренная несостоятельность ради совместимости. strictFunctionTypes делает свойства функционального типа контрвариантными, но освобождает краткую запись метода: иначе Array<Dog> перестал бы быть Array<Animal>. Нужна строгая проверка — объявляйте член свойством.
Типичные ошибки
- ✗Ждать, что
strictFunctionTypesужесточит и параметры методов - ✗Читать эту бивариантность как баг, а не как решение ради совместимости
- ✗Не понимать, что переписывание метода в свойство меняет способ его проверки
Уточняющие вопросы
- →Покажите конкретную несостоятельную программу, которую пропускает бивариантность методов.
- →Почему контрвариантность методов сломала бы присваиваемость
Array<T>?