Интероп с JavaScript
Жизнь с нетипизированным JavaScript — типы через JSDoc и @ts-check в .js-файлах, библиотеки без @types, объявление глобалов, добавленных тегом script, типизация API на Proxy и прототипный код, написанный до классов.
8 вопросов
JuniorТеорияОчень частоБиблиотека на JavaScript не поставляет типы, и пакета @types для неё нет. Какие есть варианты?
Библиотека на JavaScript не поставляет типы, и пакета @types для неё нет. Какие есть варианты?
Написать локальный файл объявлений: положить declare module 'lib' в .d.ts внутри проекта и описать только те экспорты, которыми вы реально пользуетесь. Пустой declare module 'lib'; тоже компилируется, но типизирует весь импорт как any.
Типичные ошибки
- ✗Ожидать, что компилятор сам выведет типы из исходников зависимости на JavaScript
- ✗Думать, что пустой
declare moduleдаёт настоящие типы — он лишь глушит ошибку и даётany - ✗Считать, что локально написанный
.d.tsне может описать пакет изnode_modules
Уточняющие вопросы
- →Как компилятор решает, какой
.d.tsотносится к импортуlib? - →Когда стоит опубликовать объявления в DefinitelyTyped, а не держать их локально?
JuniorТеорияЧастоЧем declare внутри модуля отличается от declare в глобальном файле-скрипте?
Чем declare внутри модуля отличается от declare в глобальном файле-скрипте?
Файл с import или export на верхнем уровне — модуль, и всё объявленное в нём ограничено этим модулем. Файл без того и другого — глобальный скрипт, его declare попадает в глобальную область. Из модуля туда ведёт только declare global { ... }.
Типичные ошибки
- ✗Думать, что
declareглобален сам по себе, независимо от того, модуль ли файл - ✗Забывать, что один
exportпревращает.d.tsв модуль, и его объявления перестают быть глобальными - ✗Считать, что
declareэмитит рантайм-привязку — он только для типов и стирается целиком
Уточняющие вопросы
- →Ваш
.d.tsперестал действовать после того, как вы добавили в негоimport. Почему? - →Когда использовать
declare global, а когда дополнять один конкретный модуль?
JuniorКодЧастоТипизация функции на чистом JavaScript через JSDoc, чтобы её проверял tsc
Типизация функции на чистом JavaScript через JSDoc, чтобы её проверял tsc
Описать форму один раз через @typedef, затем аннотировать функцию тегами @param {Type} name и @returns. Под @ts-check компилятор читает эти теги как настоящие типы, поэтому formatUser({ name: 42 }, 'yes') не соберётся — без переименования файла.
Типичные ошибки
- ✗Думать, что теги JSDoc — только документация и не могут проверить типы в
.js-файле - ✗Писать синтаксис аннотаций TypeScript внутри
.js-файла, что является ошибкой разбора - ✗Забыть
@ts-checkилиcheckJs, из-за чего теги разобраны, но ничего не проверяется
Уточняющие вопросы
- →Как выразить обобщённую функцию в JSDoc, и какой тег для этого нужен?
- →Что может выразить
.ts-файл из того, что не может JSDoc внутри.js?
MiddleКодЧастоТипизация глобала, который сторонний тег <script> добавляет в window
Типизация глобала, который сторонний тег <script> добавляет в window
Дополнить интерфейс Window слиянием объявлений: declare global { interface Window { analytics: Analytics } }. Обёртка declare global нужна модулю, чтобы дотянуться до глобальной области. Слияние добавляет член в существующий Window, и приведения не нужны.
Типичные ошибки
- ✗Опустить
declare global, из-за чего дополнение остаётся внутри модуля, и точка вызова всё ещё падает - ✗Не понимать, что один
importилиexportпревращает.d.tsв модуль - ✗Хвататься за приведение в каждой точке вызова вместо одного слияния члена в
Window
Уточняющие вопросы
- →Стоит ли делать
analyticsнеобязательным, раз скрипт вендора может не загрузиться? - →Как вместо этого типизировать глобал, который есть только в тестах?
MiddleТеорияИногдаКак типизировать JS-библиотеку, построенную на arguments и присваивании прототипа?
Как типизировать JS-библиотеку, построенную на arguments и присваивании прототипа?
Типизируйте контракт, а не реализацию. Функция, разбирающая arguments, превращается в набор перегрузок — по сигнатуре на каждое реальное соглашение о вызове — ведь компилятор проверяет точки вызова, а не тело. Конструктор на прототипе становится declare class.
Типичные ошибки
- ✗Отражать в
.d.tsрантайм-граф объектов вместо описания публичного контракта - ✗Хвататься за rest-параметр
any[]там, где набор перегрузок выражает реальные соглашения о вызове - ✗Считать, что файл объявлений ограничивает реализацию библиотеки — он ограничивает только её вызывающих
Уточняющие вопросы
- →Как выразить функцию, которая ведёт себя иначе при вызове с
new? - →Что происходит, когда написанный руками
.d.tsрасходится с реальным поведением библиотеки?
MiddleКодИногдаТипизация SDK на Proxy, чьи члены не существуют в рантайме
Типизация SDK на Proxy, чьи члены не существуют в рантайме
Объявите форму, которую прокси лишь изображает, и приведите тип один раз на границе фабрики: return new Proxy({} as Api, handler). Trap get возвращает any, поэтому компилятор никогда не проверит синтезируемые члены. Одно локальное приведение — честная цена.
Типичные ошибки
- ✗Ожидать, что компилятор выведет тип из trap-а
get, который возвращаетany - ✗Размазывать приведения по всем точкам вызова вместо одного внутри фабрики
- ✗Считать, что trap
getсрабатывает только для ключей, уже существующих на цели прокси
Уточняющие вопросы
- →Как сгенерировать интерфейс
Apiиз таблицы маршрутов через mapped-тип? - →Что сломается, если у прокси нет конечной точки, которую обещает интерфейс?
MiddleТеорияИногдаЧто даёт @ts-check вместе с JSDoc и что вы всё равно теряете по сравнению с .ts?
Что даёт @ts-check вместе с JSDoc и что вы всё равно теряете по сравнению с .ts?
Вы получаете настоящий проверяющий на .js-файле: вывод типов, сужение и объявленные через JSDoc типы применяются полностью, без переименования и нового шага сборки. Теряете синтаксис: тип живёт в комментарии, а enum, декораторы и параметры-свойства формы в JSDoc не имеют.
Типичные ошибки
- ✗Думать, что
@ts-check— фича только редактора, аtscи CI её игнорируют - ✗Считать, что JSDoc достигает полного паритета с синтаксисом
.ts - ✗Ожидать проверки JSDoc-типизированного
.jsбезcheckJsили пофайлового@ts-check
Уточняющие вопросы
- →Как написать приведение типа в JSDoc и почему ему нужны скобки?
- →В какой момент миграции JSDoc перестаёт себя окупать?
SeniorТеорияИногдаВаш класс должен наследоваться от прототипного конструктора из нетипизированной JS-библиотеки. Что ломается?
Ваш класс должен наследоваться от прототипного конструктора из нетипизированной JS-библиотеки. Что ломается?
extends требует значения, о котором компилятор знает, что оно конструируемо, поэтому базе нужен declare class. Это объявление — непроверяемое обещание: сверять его не с чем, а расхождение всплывает падением, а не ошибкой компиляции.
Типичные ошибки
- ✗Считать, что
declare classсверяется с реальной реализацией библиотеки - ✗Ожидать, что компилятор обойдёт цепочку прототипов — он видит только объявленную форму
- ✗Думать, что класс ES2015 вообще не может наследоваться от конструктора-функции
Уточняющие вопросы
- →Почему эмит в целевой
ES5ломаетextendsот встроенного типа вродеErrorилиArray? - →Как типизировать фабрику, вызываемую и с
new, и без него?