Оба описывают форму объекта и для большинства моделей и пропсов взаимозаменяемы. interface описывает только объекты и может быть открыт повторно, чтобы добавить поля (слияние объявлений, declaration merging); псевдоним type может назвать любой тип. Частый выбор по умолчанию: interface для объектов, type для всего остального.
**Только type**: объединения, кортежи, типы функций, mapped- и условные типы, результат keyof или typeof.
**Только interface**: слияние объявлений. Два блока interface Window сливаются в один, именно так .d.ts-файлы расширяют глобальные типы и типы библиотек; второй type с тем же именем даёт ошибку «Duplicate identifier».
Расширение: interface Admin extends User сообщает о несовместимом свойстве прямо в объявлении; User & {...} молча превращает его в never.
Скорость проверки: руководство TypeScript по производительности советует extends вместо &, потому что отношения между интерфейсами кэшируются, а пересечения проверяются заново.
ReactMiddle
Система типов TypeScript
type и interface, any, unknown и never, сужение типов и type guards, структурная типизация, enum и литеральные union, дженерики и ограничения, keyof и typeof, mapped и conditional types, infer, .d.ts
Закрыто вопросов
0 / 18
- 0Повторить
- 8Часто спрашивают
Вопросы
any отключает проверку типов: ему можно присвоить что угодно и делать с ним что угодно. unknown тоже принимает любое значение, но перед использованием его нужно сузить, поэтому он служит безопасным типом для непроверенных данных. never означает пустой тип: значений у него нет.
**any расползается**: loose.nmae.toUpperCase() компилируется, и всё, что прочитано из any, тоже any, поэтому одно такое значение незаметно отключает проверки дальше по коду. JSON.parse и res.json() возвращают any.
**unknown заставляет проверить** (typeof, in, type guard, схема), прежде чем читать или вызывать значение. При strict этот тип получает e в catch (e); используйте его для распарсенного JSON и вместо any в собственных API.
**never**: тип возврата функции, которая всегда бросает исключение, тип, оставшийся после обработки всех случаев (проверка полноты), и невозможные типы вроде string & number.
Верхний и нижний тип: любой тип присваивается unknown, never присваивается любому типу, а any работает в обе стороны.
Они исчезают: компилятор стирает все аннотации, интерфейсы и псевдонимы типов, и в исполняемом JavaScript от них не остаётся следа. instanceof проверяет значение во время выполнения по функции-конструктору, а у интерфейса нет значения в рантайме, поэтому TypeScript сообщает «'User' only refers to a type».
Стирание: tsc, esbuild, SWC и Babel вырезают типы и выдают обычный JavaScript; Node 24 так же запускает .ts-файлы напрямую. В рантайме типы ничего не стоят и ничего не проверяют.
**Проверки по форме вместо instanceof**: литеральное поле kind, оператор in ('name' in value) или type guard isUser(value): value is User.
Что остаётся: классы (поэтому instanceof Account работает), а также enum, namespace со значениями и parameter properties, которые генерируют код; флаг erasableSyntaxOnly (TypeScript 5.8) их запрещает.
Дженерики тоже стираются: new T() невозможен; вместо этого передают конструктор или фабрику.
Сужение означает, что TypeScript следует за вашими проверками во время выполнения по ходу кода: после if (typeof x === 'string') внутри ветки x имеет тип string, а после раннего return остаётся остаток объединения. Сужение работает по typeof, instanceof, in, сравнениям, проверке на истинность, Array.isArray и type guard.
**typeof** работает с примитивами; typeof x === 'object' оставляет null, потому что typeof null равен 'object'.
Проверка на истинность: if (name) убирает null и undefined, но вместе с ними '' и 0; сравнивайте через != null, когда эти значения допустимы.
**Сравнения и switch**: x === null, x == null (и null, и undefined) или литеральное поле размеченного объединения.
**in и instanceof**: 'href' in target оставляет варианты, у которых есть href; error instanceof TypeError сужает до класса.
Где сужение теряется: в колбэке суженная let-переменная, которую позже переприсваивают, возвращается к объявленному типу; сначала скопируйте её в const.
Объединение A | B описывает значение одного из типов, поэтому до сужения можно использовать только то, что есть у обоих. Пересечение A & B описывает значение, которое одновременно относится к обоим типам и имеет все свойства каждого. Свойство, объявленное в обоих, получает пересечение своих типов, поэтому string & number превращает его в never.
Думайте множествами значений: | допускает больше значений и гарантирует меньше свойств; & допускает меньше значений и гарантирует больше. У string & number значений нет совсем, отсюда never.
Конфликт: { id: string } & { id: number } по-прежнему компилируется, но ни один объект ему не соответствует, и ошибка появляется только там, где вы такой объект создаёте. Если конфликтует литеральный дискриминант (kind: 'circle' против kind: 'square'), всё пересечение сводится к never.
Объединения функций: ((v: string) => void) | ((v: number) => void) можно вызвать только с аргументом, который принимают обе функции, поэтому параметр становится string & number, то есть never.
Структурная типизация означает, что значение подходит под тип, если у него есть нужные свойства, что бы ещё в нём ни было, поэтому p проходит. Объектный литерал, написанный прямо в вызове, считается «свежим», и TypeScript дополнительно проверяет его на лишние свойства, потому что неизвестный ключ там почти всегда оказывается опечаткой.
Структурная, а не номинальная: типы одинаковой формы совместимы, даже если объявлены отдельно, в отличие от Java, C#, Swift и Kotlin.
Свежесть: проверка работает только для литерала, который сразу передаётся или присваивается в типизированное место; если сначала сохранить его в переменную, он становится обычным значением, и лишние ключи разрешены.
Слабые типы (weak types): тип только из необязательных свойств, например { color?: string }, отклоняет и переменную, у которой нет ни одного из них («has no properties in common»), поэтому сохранённый в переменную { colour: 'red' } тоже не проходит.
Если лишний ключ нужен: расширьте целевой тип или добавьте индексную сигнатуру; не заглушайте ошибку через as Point.
Type guard возвращает value is T: когда он возвращает true, TypeScript сужает аргумент до T в этой ветке. Функция-утверждение, asserts value is T, бросает исключение, если проверка не прошла, поэтому код после вызова уже сужен. Риск в том, что компилятор доверяет сигнатуре и никогда не проверяет тело.
Лживый guard: если isUser проверяет только 'id' in value, но заявляет value is User, вызывающие доверяют полю name, которого может не быть, и падение случается далеко от guard. Проверяйте каждое заявленное поле или выводите guard из схемы (safeParse в Zod).
**Ветка false тоже сужается**: после if (!isUser(x)) из типа x убирается User, поэтому guard, который ошибочно возвращает false, неверно типизирует ветку else.
Выведенные guard: начиная с TypeScript 5.5 filter((id) => id !== undefined) возвращает string[] без написанного вручную предиката.
Функции-утверждения подходят для инвариантов в начале функции. Вызывайте их через имя с явным типом, иначе TypeScript сообщит «Assertions require every name in the call target to be declared with an explicit type annotation».
enum относится к немногим возможностям TypeScript, которые генерируют код для рантайма, и ведёт себя не так, как остальная система типов: числовые enum отображают значения обратно в имена, строковые не принимают обычные литералы. Большинство команд используют объединение строковых литералов и объект as const для значений в рантайме.
Не стирается: enum компилируется в объект, поэтому инструменты, которые только вырезают типы, не могут его выполнить: Node падает с «TypeScript enum is not supported in strip-only mode», а флаг erasableSyntaxOnly запрещает enum.
Странности числовых enum: из-за обратного отображения Object.keys(Direction) возвращает ['0', '1', 'Up', 'Down'], а любую переменную типа number можно присвоить enum.
Строковые enum номинальны: setStatus('active') не компилируется, хотя значение ровно 'active'; вызывающим приходится импортировать enum, а данным из JSON нужен каст.
**const enum подставляется на место использования и не оставляет объекта, но для подстановки между файлами нужна вся программа, которой у пофайловых транспайлеров (esbuild, SWC, Babel) нет.
Замена**: объект as const с type Role = (typeof Role)[keyof typeof Role] даёт именованные константы и литеральное объединение, которому подходит и обычная строка из JSON.
Config равен { api: string; retries: number; features: string[] }, ConfigKey равен 'api' | 'retries' | 'features', Retries равен number, Feature даёт просто string, Role равен 'admin' | 'editor', а Dict равен string | number.
**typeof в позиции типа** берёт тип значения; в выражении typeof остаётся оператором JavaScript, который в рантайме возвращает строку вроде 'object'. Для объектного литерала TypeScript расширяет литералы, поэтому 3 становится number.
**keyof T** даёт объединение ключей T в виде литеральных типов.
**T['retries']** читает тип одного свойства; объединение ключей читает несколько (Config['api' | 'retries'] равен string | number), а T[number] читает тип элемента массива.
**Почему Feature равен string**: features расширился до string[] уже при объявлении config. as const сохраняет roles как readonly ['admin', 'editor'], поэтому [number] даёт литералы; это обычный способ получить объединение из списка значений.
**Почему в Dict есть number**: JavaScript превращает числовые ключи в строки, поэтому строковая индексная сигнатура принимает и obj[0], а keyof сообщает оба варианта.
Дайте функции два параметра типа, T для объекта и K extends keyof T для ключа, и верните T[K]. Ограничение говорит, что K должен быть одним из ключей T, поэтому неверный ключ даёт ошибку компиляции, а результат имеет тип именно этого свойства.
Работу делает вывод типов: getProperty(user, 'age') выводит T из user, а K как литерал 'age', поэтому результат имеет тип number; аргументы типа никто не пишет.
Ограничения в целом: T без ограничения внутри функции ведёт себя как unknown; T extends { id: string } позволяет телу читать item.id, а вызывающий сохраняет свой полный тип.
**const-параметры типа** (TypeScript 5.0) выводят литеральные типы так, будто вызывающий написал as const: paths(['/', '/settings']) остаётся readonly ['/', '/settings'].
Параметр типа должен встречаться дважды: параметр, который ничего не связывает, например в function len<T extends string>(s: T): number, ничем не лучше (s: string) и только сложнее читается.
Перегрузки представляют собой несколько сигнатур вызова над одной реализацией: вызывающий видит только перегрузки, а реализация должна обработать их все. Они окупаются, когда тип результата зависит от переданных аргументов; если не зависит, одна сигнатура с параметром-объединением проще.
Сигнатура реализации скрыта: load(arg: string | string[]) нельзя вызвать напрямую, и она должна быть совместима с каждой перегрузкой над ней.
Выбор идёт сверху вниз: TypeScript берёт первую подходящую перегрузку, поэтому конкретные ставят первыми.
Аргумент-объединение не подходит ни под одну перегрузку: при перегрузках для string и string[] вызывающий с string | string[] получит «No overload matches this call». Поэтому handbook советует по возможности предпочитать параметры-объединения.
Тело не проверяется против каждой перегрузки: внутри параметр имеет широкий тип объединения, и ветка, которая возвращает User для массива id, всё равно компилируется.
Файл .d.ts содержит только объявления типов и никакого кода: он описывает JavaScript, который существует в другом месте. Для нетипизированного пакета установите @types/<name> или напишите declare module '<name>'; чтобы добавить глобальное свойство вроде window.analytics, расширьте интерфейс Window внутри declare global.
Откуда берутся типы: пакеты на TypeScript поставляют свои .d.ts (types в package.json); для остальных есть типы сообщества в DefinitelyTyped (@types/lodash); API браузера описаны в lib.dom.d.ts.
Быстрая заглушка: одна строка declare module 'legacy-chart'; делает любой импорт из пакета any; если объявить функции, которые вы используете, проверки сохраняются.
Модуль или скрипт: .d.ts с import или export на верхнем уровне (даже export {}) считается модулем, и его объявления остаются локальными, поэтому глобальные добавления пишут внутри declare global { ... }. В файле без импортов и экспортов interface Window на верхнем уровне расширяет глобальный интерфейс напрямую.
Файл должен входить в программу (его покрывает include в tsconfig.json), иначе его объявления игнорируются.
Mapped type строит объектный тип, проходя циклом по объединению ключей: { [K in Keys]: T[K] }. Pick проходит по выбранным ключам, Omit по всем ключам, переназначая ненужные в never, а DeepPartial делает каждое свойство необязательным и рекурсивно спускается во вложенные объекты.
Модификаторы: можно добавить readonly или ? либо убрать их через -readonly и -?; так устроены Required и хелпер Mutable.
Гомоморфный mapped type сохраняет модификаторы: проход по keyof T (или по K extends keyof T) сохраняет у каждого свойства ? и readonly, поэтому в MyPick поле id остаётся read-only.
**Переназначение ключей через as** (TypeScript 4.1): ключ, переназначенный в never, выбрасывается, а as get${Capitalize<K & string>} переименовывает ключи.
**Встроенный Omit нестрогий**: он принимает любой ключ (K extends keyof any), поэтому Omit<User, 'idd'> молча ничего не убирает; версия здесь ограничивает K extends keyof T и ловит опечатку.
**Крайние случаи DeepPartial**: функции и даты тоже объекты; без ветки для функций onSave превратился бы в {}, а Date всё равно становится набором необязательных методов, поэтому реальные версии обрабатывают такие типы отдельно.
Условный тип T extends U ? X : Y выбирает тип в зависимости от того, присваивается ли T типу U, как тернарный оператор для типов. Внутри extends конструкция infer R объявляет переменную типа, которую TypeScript заполняет сопоставлением, поэтому T extends (...args: any[]) => infer R ? R : never достаёт тип результата.
Разворачивание: T extends Promise<infer V> ? UnwrapPromise<V> : T рекурсивно проходит Promise<Promise<number>> до number; встроенный Awaited делает то же самое и вдобавок понимает любой thenable.
**Другие места для infer**: элемент массива ((infer E)[]), первый элемент кортежа ([infer H, ...unknown[]]), параметры функции или часть строки в template literal type.
Стандартная библиотека устроена так же: ReturnType, Parameters, InstanceType, Awaited, Exclude и Extract все являются условными типами.
Держите их в утилитарных типах: вложенные условия замедляют проверку и дают нечитаемые ошибки; в коде приложения именованный интерфейс понятнее.
Template literal types строят строковые литеральные типы с синтаксисом обратных кавычек: on${Capitalize<E>} превращает 'click' в 'onClick', а объединение разворачивается во все комбинации. Вместе с переназначением ключей в mapped type они генерируют целый объект пропсов-обработчиков из таблицы событий.
Встроенные хелперы: Capitalize, Uncapitalize, Uppercase и Lowercase преобразуют литеральные типы.
Объединения перемножаются: ${'sm' | 'lg'}-${'red' | 'blue'} даёт четыре литерала; примерно на 100 000 комбинаций TypeScript сдаётся с ошибкой «Expression produces a union type that is too complex to represent».
Шаблоны, а не только имена: ${number}px принимает '12px' и отклоняет '12'; --${string} типизирует CSS custom properties; /users/${string} типизирует роут.
**infer работает внутри**: H extends on${infer E} ? Uncapitalize<E> : never возвращает от 'onSelect' к 'select'; типизированные роутеры так же извлекают параметры :id из строки пути.
**K & string**: Capitalize требует string, а keyof T может содержать ещё и ключи number и symbol, поэтому сначала пересеките его со string.
A равен string[] | number[], B равен (string | number)[], C равен never, а D равен never[]. Условный тип над голым параметром типа дистрибутивен: он выполняется для каждого варианта объединения отдельно и объединяет результаты. [T] уже не голый параметр, поэтому объединение проверяется целиком.
Почему происходит распределение
TypeScript разбивает объединение, только когда оно подставляется в голый параметр типа слева от extends. Если написать напрямую, string | number extends string ? 'y' : 'n' даёт 'n'; через IsString<T> = T extends string ? 'y' : 'n' выражение IsString<string | number> даёт 'y' | 'n'.
never представляет собой пустое объединение, поэтому распределение по нему выполняется ноль раз и возвращает never: это и есть C. По той же причине ломается наивный IsNever<T> = T extends never ? true : false, который возвращает never вместо true.
boolean является объединением true | false, поэтому ToArray<boolean> даёт false[] | true[], и это сюрприз, когда тип флага проходит через дистрибутивный хелпер.
Какое поведение нужно
Распределение нужно для работы с каждым вариантом: фильтрации объединения (Exclude<T, U> равен T extends U ? never : T) или преобразования каждого варианта размеченного объединения по отдельности.
**Обёртка в [T]** нужна для вопросов о типе целиком: IsNever, «это ровно boolean?», или когда объединение должно остаться одним типом массива, как в B.
Пересеките базовый тип со свойством-меткой, которого нет ни у одного настоящего значения: type UserId = string & { readonly [brand]: 'UserId' }. У обычной string или у OrderId такой метки нет, поэтому передать их туда, где ждут UserId, не получится, а значения с меткой создаёт только одна функция, которая проверяет и делает каст.
Зачем это нужно
TypeScript структурный, поэтому type UserId = string остаётся просто другим именем: getOrders(orderId) компилируется, и перепутанные id превращаются в баг в продакшене, например загрузку или удаление не той записи.
Как это работает
Метка существует только в типе. В рантайме значение остаётся обычной строкой, поэтому метка ничего не стоит, а значение сериализуется как есть.
Ключ unique symbol (declare const brand: unique symbol) не может совпасть с настоящим свойством, в отличие от строкового ключа __brand.
Значение с меткой по-прежнему является string, поэтому userId.toUpperCase() работает; заблокировано только обратное направление, из string в UserId.
Цена
На каждой границе (ответы API, параметры роута, ввод в форме) нужно преобразование. Схема делает его за один шаг: z.string().brand<'UserId'>() в Zod и проверяет, и ставит метку.
Тем же приёмом моделируют единицы измерения и проверенные значения: Cents, Milliseconds, Email, NonEmptyString.
Ковариантность сохраняет направление подтипов: Dog[] можно использовать как Animal[]. Контравариантность его разворачивает: обработчик любого Animal может заменить обработчик Dog. С strictFunctionTypes параметры функций контравариантны, а параметры методов остаются бивариантными, чтобы массивы оставались ковариантными.
Правила
Типы результата ковариантны: функция, возвращающая Dog, подходит туда, где ждут функцию, возвращающую Animal.
Параметры контравариантны: там, где ждут (a: Animal) => void, функция (d: Dog) => void небезопасна, потому что вызывающий может передать Cat. strictFunctionTypes (часть strict) отклоняет её для свойств с типом функции, переменных и колбэков.
Методы бивариантны: член, записанный синтаксисом метода, handle(a: Animal): void, принимает оба направления. Строгая проверка методов сделала бы Dog[] неприсваиваемым к Animal[], потому что методы Array<T>, например push(item: T), принимают T параметром.
Последствия
Обе дыры настоящие: m.handle(cat) компилируется и бросает «dog.bark is not a function», а animals.push(cat) кладёт кошку в dogs.
В собственных типах объявляйте колбэки как свойства, onSelect: (item: T) => void, чтобы получить строгую проверку; правило method-signature-style в typescript-eslint это закрепляет.
in и out у параметров типа (TypeScript 4.7) явно задают вариантность; компилятор сверяет их со структурой и может пропустить структурное сравнение в больших дженерик-типах.