
type vs. interface
Типы могут быть большими, их не всегда удобно передавать.

Поэтому можем вынести тип в отдельную переменную.

Это нам позволяет переиспользовать типы.
Также вместо types можем использовать interface.

Отличия:
1) Интерфейс описывает исключительно объект.
Type может описывать простые типы – string, number.
2) Extend
Расширение interface.

Можем сделать tail необязательным.

Расширение type.

3) Возможность которая есть у interface, но нету у типов.
Например, мы захотим дополнить interface.
Просто напишем еще раз интерфейс с другими полями, и они объединятся.
То есть interface мержатся.

С тайпами так делать нельзя.
У нас будет ругаться на то, что мы два раза написали type Dog.

Итог.

AS
as в TypeScript — это утверждение типа (type assertion). Оно говорит компилятору: «поверь, что это значение имеет такой тип». На выполнение кода это не влияет, только на проверку типов.
Пример-аналогия: у тебя есть коробка без надписи. Ты наклеил ярлык «книги». Компилятор теперь верит, что внутри книги — и даёт автодополнение/проверки, как для книг. Но если там кирпич — программа упадёт уже во время выполнения.
Когда это уместно
- Компилятор не смог догадаться о типе, а ты точно знаешь его.
- Ты привёл данные к нужному виду (проверил, распарсил) — и хочешь, чтобы TS это понял.
- TS не может вывести тип, а ты его знаешь.
Например, послеJSON.parseили ответа «чужой» либы.
type Api = { ok: true; data: number[] } | { ok: false; error: string };
const api = JSON.parse(text) as Api;
// или:
declare const lib: any;
const user = lib.getUser() as { id: number; name: string };
Вот различие
Вариант 1.
type ApiResponse =
| { ok: true; data: { id: number }[] }
| { ok: false; error: string };
const text = '{"ok": true, "data": [{"id": 1}]}';
const resp = JSON.parse(text) as ApiResponse;
Вариант 2.
type ApiResponse =
| { ok: true; data: { id: number }[] }
| { ok: false; error: string };
const text = '{"ok": true, "data": [{"id": 1}]}';
const resp: ApiResponse = JSON.parse(text);
Разница по сути
: ApiResponse— аннотация. TS проверяет: “а то, что справа, совместимо с ApiResponse?”
Если справа будет, например,unknown, то ошибка.as ApiResponse— утверждение. Ты говоришь компилятору “поверь, что это ApiResponse” (и он верит, особенно если справаany/unknown).
Где увидишь различие
1) Когда справа unknown (или неявно несовместимый тип):
declare function parseJson(s: string): unknown;
const a: ApiResponse = parseJson(text); // ❌ ошибка (unknown → ApiResponse)
const b = parseJson(text) as ApiResponse; // ✅ ок (ты утвердил тип)
2) Когда нужно типизировать часть большого выражения:
doSomething((JSON.parse(text) as ApiResponse).data); // удобно «внутри» выражения
3) Опасное «продавливание» несовместимых типов:
declare function get(): string | number;
const x: number = get(); // ❌ TS ругается (может прийти string)
const y = get() as number; // ✅ компилируется, но может упасть в рантайме
Практика
- Если можно — пиши аннотацию
: ApiResponse(TS проверит совместимость). - Используй
as ApiResponse, когда:- TS не может вывести тип (возвращается
any/unknown), - ты уже гарантировал форму данных (или дальше валидируешь),
- нужно типизировать часть выражения.
- TS не может вывести тип (возвращается
Generics
Генерики (Generics) — это «параметры типов». Как функции принимают значения и возвращают новое значение, так генерики принимают типы и возвращают новые типы. Это даёт переиспользуемость и строгую типизацию без any.
Зачем
- Пишем один раз — используем с разными типами.
- Получаем автодополнение и проверки, сохраняя конкретику типа (без потери в
any).
Коротко: Generics (<T>) нужны, когда функция/тип должен работать с разными типами и при этом сохранять точную связь между входом и выходом. Если вы ставите просто :number или :string, вы «прибиваете» тип — гибкость и точность теряются.
Вот чем <T> лучше фиксированных типов и any:
1) Переиспользование без копипасты
Без дженериков пришлось бы писать две версии или объединения типов.
// Плохо: теряется точность
function head1(a: string[] | number[]) {
return a[0]; // тип: string | number (неудобно)
}
// Хорошо: с дженериком сохраняем точный тип
function head2<T>(a: T[]): T | undefined {
return a[0];
}
const s = head2(["a", "b"]); // s: string | undefined
const n = head2([1, 2]); // n: number | undefined
2) Сохранение связей «вход → выход»
Generics позволяют сказать: «что дали — то и получили».
function identity<T>(x: T): T { return x; }
const a = identity("hi"); // string
const b = identity(42); // number
3) Безопасные «пары» типов
Можно запретить неправильные комбинации ещё на этапе типов.
function equals<T>(a: T, b: T): boolean {
return a === b;
}
equals(1, 1); // ок
// equals(1, "1"); // ошибка уже при компиляции
С обычными :number | :string такого не добиться одной сигнатурой.
Ограничения (extends)
Можно ограничить допустимые типы и давать значения по умолчанию:
function len<T extends { length: number }>(x: T) {
return x.length;
} // теперь T обязан иметь length
len("abc"); // ok
len([1,2,3]); // ok
// len(123); // ошибка: нет length
Читаем так: «T — это любой тип, у которого есть свойство length: number».
Значит, подойдут строки, массивы, Map, Set и т. п. — у них есть length (или совместимое).
len("abc"); // ок — у строки есть length
len([1,2,3]); // ок — у массива есть length
// len(123); // ошибка — у числа нет length
Связанные типы через keyof
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
keyof T= «множество названий полей объектаT».
Например, еслиT = { id: number; name: string }, тоkeyof Tэто"id" | "name".K extends keyof Tзначит «Kдолжен быть одним из существующих ключей объектаT».
Поэтому нельзя обратиться к несуществующему полю — компилятор поймает ошибку.- Возврат
T[K]= «тип значения по ключуK».
ЕслиK = "id", вернётсяnumber; еслиK = "name", вернётсяstring.
Пример:
const u = { id: 1, name: "Ada" };
const id = getProp(u, "id"); // тип: number
const name = getProp(u, "name"); // тип: string
// getProp(u, "age"); // ошибка: такого ключа нет
Типы и интерфейсы с дженериками
type ApiOk<T> = { ok: true; data: T };
type ApiErr = { ok: false; error: string };
type ApiResult<T> = ApiOk<T> | ApiErr;
interface Box<T> { value: T }
class Repo<T> {
private items: T[] = [];
add(x: T) { this.items.push(x); }
}
<T> — это просто место для типа, как пустое поле, которое вы позже заполняете: T = User, или T = number[], и т.д.
1) ApiOk<T>, ApiErr, ApiResult<T>
Делаем общий тип для ответа API:
ApiOk<T>— успешный ответ:ok: trueи данные любого типаT.ApiErr— ошибка:ok: falseи строкаerror.ApiResult<T>— либо успех с даннымиT, либо ошибка.
Примеры:
type ApiOk<T> = { ok: true; data: T };
type ApiErr = { ok: false; error: string };
type ApiResult<T> = ApiOk<T> | ApiErr;
type User = { id: number; name: string };
// Успех с пользователем
const r1: ApiResult<User> = { ok: true, data: { id: 1, name: "Ada" } };
// Ошибка
const r2: ApiResult<User> = { ok: false, error: "Not found" };
// Использование: TS понимает, что если ok === true, есть data нужного типа
function showUser(res: ApiResult<User>) {
if (res.ok) {
// здесь res: ApiOk<User>, значит res.data: User
console.log(res.data.name);
} else {
// здесь res: ApiErr
console.error(res.error);
}
}
Смысл: один шаблон «успех/ошибка» можно применить к любой форме данных — пользователи, товары, массив чисел и т.д., при этом типы остаются точными.
2) interface Box<T>
Это просто «коробка», которая хранит что-то типа T.
interface Box<T> { value: T }
const n: Box<number> = { value: 123 }; // коробка с числом
const s: Box<string> = { value: "hi" }; // коробка со строкой
Значение по умолчанию
type paging<T = unknown> = { items: T[]; page: number; total: number };
Когда <T> не нужен
- Функция работает строго с одним типом (например, сугубо числа) — ставьте
:number. - Нет зависимости между аргументами и результатом — иногда достаточно объединений
string | number.
Правило большого пальца
- Если логика одинакова для разных типов и важно сохранить точные типы на выходе — используйте Generics.
- Если тип строго один — ставьте конкретный тип.
- Никогда не подменяйте дженерики на
any, если нужна типобезопасность.
Пример с курса

Проблема в том, что может прийти намбер, а вернутся строка и наоборот.
Надо писать 2 отдельных функции.

Но мы дублируем код.
Перепишем с помощью Generics.

Так будет ругаться.

Можем передавать 2 типа.

Мы можем уточнить что <T> это объект или другой тип.

Примеры с моих проектов
1) Пересечение типов
export type MenuItem = {
id: string;
label: string;
url: string;
parentId: string | null;
childItems?: { nodes: MenuItem[] };
};
// Это пересечение типов. NormalizedItem должен иметь все поля из MenuItem и дополнительное обязательное поле path: string».
type NormalizedItem = MenuItem & { path: string };
Читай так: «NormalizedItem должен иметь все поля из MenuItem и дополнительное обязательное поле path: string».
Это похоже на
interface NormalizedItem extends MenuItem { path: string }
— для объектных типов это эквивалентно. Разница лишь в синтаксисе: & — пересечение через type, extends — через interface.
Практичные задачи
Сайт: https://typescript-exercises.github.io/#exercise=2&file=%2Findex.ts
Задача 2
interface User {
name: string;
age: number;
occupation: string;
}
interface Admin {
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
name: 'Max Mustermann',
age: 25,
occupation: 'Chimney sweep'
},
{
name: 'Jane Doe',
age: 32,
role: 'Administrator'
},
{
name: 'Kate Müller',
age: 23,
occupation: 'Astronaut'
},
{
name: 'Bruce Willis',
age: 64,
role: 'World saver'
}
];
export function logPerson(user: Person) {
console.log(` - ${user.name}, ${user.age}`);
}
persons.forEach(logPerson);
Из интересного:
- Типу можно присвоить интерфейс. export type Person = User | Admin;
Задача 3
interface User {
name: string;
age: number;
occupation: string;
}
interface Admin {
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
name: 'Max Mustermann',
age: 25,
occupation: 'Chimney sweep'
},
{
name: 'Jane Doe',
age: 32,
role: 'Administrator'
},
{
name: 'Kate Müller',
age: 23,
occupation: 'Astronaut'
},
{
name: 'Bruce Willis',
age: 64,
role: 'World saver'
}
];
export function logPerson(person: Person) {
let additionalInformation: string;
if (person.role) {
additionalInformation = person.role;
} else {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
persons.forEach(logPerson);
Сейчас такие ошибки:
- index.ts(58,16): error TS2339: Property ‘role’ does not exist on type ‘Person’. Property ‘role’ does not exist on type ‘User’.
- index.ts(59,40): error TS2339: Property ‘role’ does not exist on type ‘Person’. Property ‘role’ does not exist on type ‘User’.
- index.ts(61,40): error TS2339: Property ‘occupation’ does not exist on type ‘Person’. Property ‘occupation’ does not exist on type ‘Admin’.

Почему не работает if (person.role)?
Потому что до сужения TypeScript видит person как User | Admin, а у User нет поля role. Нельзя обращаться к свойству, которого может не быть, пока TS не убедится, что это именно Admin.
Для объединений вроде type Person = User | Admin нужно сузить тип перед доступом к полям, которые есть только в одной из веток. Проще всего — оператором in.
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing
'role' in person — родной оператор JavaScript (есть давно в JS). Он проверяет, есть ли у объекта такое свойство — либо своё, либо унаследованное по прототипу.
В TypeScript та же запись ещё и выступает тайпгардом (сужает тип объединения).
interface User {
name: string;
age: number;
occupation: string;
}
interface Admin {
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
name: 'Max Mustermann',
age: 25,
occupation: 'Chimney sweep'
},
{
name: 'Jane Doe',
age: 32,
role: 'Administrator'
},
{
name: 'Kate Müller',
age: 23,
occupation: 'Astronaut'
},
{
name: 'Bruce Willis',
age: 64,
role: 'World saver'
}
];
export function logPerson(person: Person) {
let additionalInformation: string;
if ('role' in person) {
additionalInformation = person.role;
} else {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
persons.forEach(logPerson);
Задача 4
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' }
];
export function isAdmin(person: Person) {
return person.type === 'admin';
}
export function isUser(person: Person) {
return person.type === 'user';
}
export function logPerson(person: Person) {
let additionalInformation: string = '';
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
console.log('Admins:');
persons.filter(isAdmin).forEach(logPerson);
console.log();
console.log('Users:');
persons.filter(isUser).forEach(logPerson);
Ошибка:
- index.ts(52,40): error TS2339: Property ‘role’ does not exist on type ‘Person’. Property ‘role’ does not exist on type ‘User’.
- index.ts(55,40): error TS2339: Property ‘occupation’ does not exist on type ‘Person’. Property ‘occupation’ does not exist on type ‘Admin’.

In case you are stuck:
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
Decision:

Это называется type guard.
Супер просто:
Person = User | Admin— это «или пользователь, или админ». TypeScript не знает, кто именно в переменной.- Когда функция
isAdmin(person)возвращает простоboolean, TS послеif (isAdmin(person))всё ещё видит тип Person (не сузился) → свойствоroleнедоступно. - Когда ты пишешь
isAdmin(person): person is Admin, ты говоришь TS: «если функция вернулаtrue, то это точно Admin». Это называется type guard.
Итог: с person is Admin внутри if тип сужается до Admin, и person.role становится доступным. Аналогично с person is User и person.occupation.
Задача 5
/*
Intro:
Time to filter the data! In order to be flexible
we filter users using a number of criteria and
return only those matching all of the criteria.
We don't need Admins yet, we only filter Users.
Exercise:
Without duplicating type structures, modify
filterUsers function definition so that we can
pass only those criteria which are needed,
and not the whole User information as it is
required now according to typing.
Higher difficulty bonus exercise:
Exclude "type" from filter criteria.
*/
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{
type: 'admin',
name: 'Jane Doe',
age: 32,
role: 'Administrator'
},
{
type: 'user',
name: 'Kate Müller',
age: 23,
occupation: 'Astronaut'
},
{
type: 'admin',
name: 'Bruce Willis',
age: 64,
role: 'World saver'
},
{
type: 'user',
name: 'Wilson',
age: 23,
occupation: 'Ball'
},
{
type: 'admin',
name: 'Agent Smith',
age: 23,
role: 'Administrator'
}
];
export const isAdmin = (person: Person): person is Admin => person.type === 'admin';
export const isUser = (person: Person): person is User => person.type === 'user';
export function logPerson(person: Person) {
let additionalInformation = '';
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
export function filterUsers(persons: Person[], criteria: User): User[] {
return persons.filter(isUser).filter((user) => {
const criteriaKeys = Object.keys(criteria) as (keyof User)[];
return criteriaKeys.every((fieldName) => {
return user[fieldName] === criteria[fieldName];
});
});
}
console.log('Users of age 23:');
filterUsers(
persons,
{
age: 23
}
).forEach(logPerson);
// In case you are stuck:
// https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#predefined-conditional-types
Задачка на Partial<Type> – когда передаем только часть типа.
В задачке у нас Interface User, но в функцию мы передаем только одно его свойство.
filterUsers(
persons,
{
age: 23
}
).forEach(logPerson);
Но сейчас мы ождидаем, что должен прийти весь объект User, а не только age.
export function filterUsers(persons: Person[], criteria: User): User[] {
return persons.filter(isUser).filter((user) => {
const criteriaKeys = Object.keys(criteria) as (keyof User)[];
return criteriaKeys.every((fieldName) => {
return user[fieldName] === criteria[fieldName];
});
});
}
Исправим это с помощью Partial<User>.
export function filterUsers(persons: Person[], criteria: Partial<User>): User[] {
return persons.filter(isUser).filter((user) => {
const criteriaKeys = Object.keys(criteria) as (keyof User)[];
return criteriaKeys.every((fieldName) => {
return user[fieldName] === criteria[fieldName];
});
});
}
Вот документация: https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype