TypeScript представляет собой мощное расширение JavaScript, которое добавляет статическую типизацию в этот динамический язык. В JavaScript, где переменная может свободно менять тип в процессе выполнения программы, TypeScript вводит строгий порядок и структуру, позволяя разработчикам явно указывать, какие типы данных ожидаются и используются в коде.
Почему типы важны в разработке
Типизация – не просто формальность или дополнительное ограничение. Это фундаментальный инструмент, который кардинально меняет процесс разработки. Прежде всего, статическая типизация значительно снижает вероятность ошибок. Когда вы четко определяете, что функция принимает число и возвращает строку, TypeScript не даст передать ей объект или массив без явного преобразования.
TypeScript | 1
2
3
4
5
6
| function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}
// Ошибка: аргумент типа string не может быть присвоен параметру типа number
formatPrice("100"); |
|
Типы также работают как документация, которая никогда не устаревает. Взглянув на сигнатуру функции или интерфейс объекта, разработчик мгновенно понимает, с какими данными ему предстоит работать. Это особенно ценно в больших проектах, где код может читать и модифицировать множество людей. Автодополнение в редакторах кода – еще одно преимущество, которое обеспечивает типизация. Интегрированные среды разработки (IDE) могут предлагать подходящие методы и свойства для объектов, экономя время и снижая количество опечаток.
Почему typescript не проверяет типы при использовании спред-оператора? Никак не могу разобраться, почему не работает проверка типов в следующем примере:
interface... TypeScript vs Script# vs У кого какой опыт ? - сравнительные достоинства и недостатки. Перевод C# на TypeScript Доброго времени суток))) (Извините если не в ту тему)
Существует рабочая программы для локального... VS2012 + typescript 9.1.1 При работе с TypeScript VS2012 виснет или закрывается регулярно, никакой конкретной информации об...
Отличия от JavaScript
JavaScript – язык с динамической типизацией, где переменная может содержать данные любого типа, и этот тип может меняться в процессе выполнения программы:
JavaScript | 1
2
3
| let value = 42; // Сначала value содержит число
value = "Hello"; // Теперь value содержит строку
value = { id: 1 }; // А теперь – объект |
|
TypeScript вводит контроль на этапе компиляции, заставляя разработчиков явно указывать тип переменной или позволяя компилятору вывести его:
TypeScript | 1
2
| let value: number = 42;
value = "Hello"; // Ошибка: тип string не может быть присвоен типу number |
|
При этом TypeScript является супермножеством JavaScript – любой валидный JavaScript код является также валидным TypeScript кодом. Это означает, что внедрение TypeScript в существующие проекты может происходить постепенно, файл за файлом.
История развития типизации в TS
TypeScript был создан Microsoft в 2012 году, когда Андерс Хейлсберг (также известный как создатель C#) осознал необходимость в инструменте, который бы делал разработку JavaScript более масштабируемой для крупных приложений. С тех пор система типов эволюционировала от базовых примитивов и интерфейсов до сложных конструкций, включающих условные типы, мэппинг типов и дженерики. Каждая новая версия TypeScript расширяла возможности системы типов:
TypeScript 2.0 (2016) ввел null и undefined как отдельные типы и добавил строгую проверку null,
TypeScript 2.1 добавил инференцию для сужения типов (type narrowing),
TypeScript 3.0 (2018) улучшил работу с параметрами rest и spread,
TypeScript 4.0 (2020) представил синтетические типы и улучшения для кортежей.
Сравнение TypeScript с другими языками со статической типизацией
В отличие от таких языков как Java или C#, где типизация обязательна, TypeScript предоставляет возможность постепенного внедрения типов. Система типов TS также более гибкая – она включает структурную типизацию вместо номинальной, используемой в большинстве объектно-ориентированных языков. При структурной типизации два типа считаются совместимыми, если их структура совпадает, независимо от их имен:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| interface Named {
name: string;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Допустимо, т.к. Person имеет свойство name
const p: Named = new Person("Alice"); |
|
В сравнении с Flow (альтернативная система типов для JavaScript от Facebook), TypeScript имеет более активное сообщество, лучшую экосистему инструментов и более глубокую интеграцию с популярными фреймворками. TypeScript также отличается от языков со статической типизацией, таких как Scala или Haskell, меньшей математической строгостью, но большей практичностью и доступностью для среднего разработчика. В итоге, TypeScript занимает уникальную нишу между гибкостью динамически типизированных языков и надежностью статически типизированных, предлагая разумный компромисс для современной веб-разработки.
Базовые типы
TypeScript предлагает набор основных типов данных, которые составляют фундамент для построения более сложных конструкций. Освоение этих базовых типов — необходимый шаг к полноценному использованию всех преимуществ статической типизации.
Примитивные типы: string, number, boolean
В основе системы типов TypeScript лежат три фундаментальных примитива, унаследованных от JavaScript:
String — для работы с текстовыми данными. В TypeScript строки обозначаются ключевым словом string :
TypeScript | 1
2
| let name: string = "Александр";
let greeting: string = `Привет, ${name}!`; // Шаблонные строки также поддерживаются |
|
Number — для представления числовых значений. В TypeScript, как и в JavaScript, все числа представлены в формате с плавающей точкой:
TypeScript | 1
2
3
4
| let decimal: number = 42;
let float: number = 3.14;
let hex: number = 0xf00d; // шестнадцатеричная запись
let binary: number = 0b1010; // двоичная запись |
|
Boolean — простейший тип, принимающий только значения true или false :
TypeScript | 1
2
| let isCompleted: boolean = false;
isCompleted = true; |
|
Особый тип any и когда его избегать
Тип any в TypeScript позволяет переменной принимать значения любых типов, фактически отключая проверку типов:
TypeScript | 1
2
3
| let dynamicValue: any = 4;
dynamicValue = "строка"; // Допустимо
dynamicValue = { key: "value" }; // Тоже допустимо |
|
Использование any подрывает основное преимущество TypeScript — статическую типизацию. Код с избыточным применением этого типа теряет в надежности и поддерживаемости.
Тип any стоит применять только в исключительных случаях:- При работе с кодом, не имеющим типизации (например, сторонние JavaScript-библиотеки без определений типов).
- В процессе постепенной миграции JavaScript-проекта на TypeScript.
- Когда тип действительно неизвестен и может меняться непредсказуемо.
В большинстве ситуаций лучше использовать более конкретные типы или конструкции вроде union-типов.
Практические примеры использования
Рассмотрим практические примеры сочетания различных базовых типов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Функция с типизированными параметрами и возвращаемым значением
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
// Объект с указанием типов свойств
let product: { id: number; name: string; inStock: boolean } = {
id: 1,
name: "Ноутбук",
inStock: true
};
// Массив чисел
let fibonacci: number[] = [1, 1, 2, 3, 5, 8, 13];
// Функциональный тип (сигнатура функции)
let logger: (message: string) => void;
logger = (message) => console.log(`LOG: ${message}`); |
|
Тип null и undefined: особенности и применение
TypeScript различает два специальных типа — null и undefined , отражающие соответствующие значения в JavaScript:
TypeScript | 1
2
| let empty: null = null;
let notDefined: undefined = undefined; |
|
По умолчанию эти типы являются подтипами всех других типов, что может привести к неожиданным ошибкам. Чтобы сделать их обработку более строгой, в tsconfig.json можно включить параметр strictNullChecks :
JSON | 1
2
3
4
5
| {
"compilerOptions": {
"strictNullChecks": true
}
} |
|
При строгой проверке null необходимо явно указывать возможность нулевого значения с помощью union-типа:
TypeScript | 1
2
3
4
5
| // При включенном strictNullChecks это вызовет ошибку
let userName: string = null;
// Правильный способ
let userNameOrNull: string | null = null; |
|
Литеральные типы и их практическая ценность
Литеральные типы позволяют указать, что переменная может принимать только конкретное значение или набор значений:
TypeScript | 1
2
3
4
5
6
7
8
9
| // Переменная может содержать только конкретную строку
let direction: "north" | "south" | "east" | "west";
direction = "north"; // Допустимо
direction = "northeast"; // Ошибка: Type '"northeast"' is not assignable
// Числовые литералы
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
diceRoll = 3; // Допустимо
diceRoll = 7; // Ошибка |
|
Литеральные типы особенно полезны для создания типов-перечислений и API с ограниченным набором значений параметров. Они делают код более предсказуемым и улучшают автодополнение.
Работа с массивами и кортежами в TypeScript
TypeScript предлагает два способа объявления массивов:
TypeScript | 1
2
3
4
5
| // С использованием обобщенного типа Array
let fruits: Array<string> = ["яблоко", "банан", "груша"];
// С использованием синтаксиса квадратных скобок
let vegetables: string[] = ["морковь", "капуста", "свекла"]; |
|
Оба варианта эквивалентны, но второй более компактен и чаще встречается в кодовых базах.
Кортежи (tuples) — особый вид массивов с фиксированным количеством элементов, где каждая позиция имеет определенный тип:
TypeScript | 1
2
3
4
5
| // Кортеж, где первый элемент — строка, а второй — число
let nameAndAge: [string, number] = ["Антон", 34];
// Деструктуризация кортежа
let [name, age] = nameAndAge; |
|
Кортежи удобны для представления структурированных данных, когда нужно сохранить типы отдельных элементов, но создание полноценного интерфейса избыточно:
TypeScript | 1
2
3
4
5
6
7
| // Функция, возвращающая кортеж
function getUserInfo(): [string, number, boolean] {
// Возвращаем имя, возраст и статус активности
return ["Мария", 28, true];
}
const [userName, userAge, isActive] = getUserInfo(); |
|
Однако для более сложных структур данных кортежи могут затруднять понимание кода, и в таких случаях лучше использовать интерфейсы или классы с именованными свойствами. Для эффективной работы с TypeScript недостаточно знать только примитивные типы. В арсенале разработчика должны быть и другие базовые типы, которые помогают создавать надёжные и выразительные программы.
Тип unknown: безопасная альтернатива any
Тип unknown был добавлен в TypeScript как более безопасная альтернатива типу any . В отличие от any , unknown требует явной проверки типа перед выполнением операций:
TypeScript | 1
2
3
4
5
6
7
8
9
10
| let userInput: unknown;
let userName: string;
userInput = "Иван";
// userName = userInput; // Ошибка: переменная типа unknown не может быть присвоена переменной типа string
// Необходима проверка типа перед использованием
if (typeof userInput === "string") {
userName = userInput; // Теперь это допустимо
} |
|
Это особенно полезно, когда вы работаете с данными, тип которых заранее неизвестен (например, пользовательский ввод или ответ API), но требуется гарантировать безопасность типов в дальнейшем коде.
Тип void: отсутствие значения
Тип void обычно используется в качестве возвращаемого значения функций, которые ничего не возвращают:
TypeScript | 1
2
3
4
5
6
7
| function logMessage(message: string): void {
console.log(message);
// Без оператора return или с пустым return
}
// Переменные типа void могут хранить только undefined или null (при выключенном strictNullChecks)
let unusable: void = undefined; |
|
Тип never: невозможные значения
Тип never представляет значения, которые никогда не могут возникнуть. Он используется в нескольких сценариях:
1. Функции, которые никогда не завершаются (бросают исключения или содержат бесконечные циклы):
TypeScript | 1
2
3
4
5
6
7
8
9
| function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// Что-то делаем бесконечно
}
} |
|
2. В сужении типов, когда все возможные варианты исчерпаны:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function assertNever(value: never): never {
throw new Error(`Недопустимое значение: ${value}`);
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if ('radius' in shape) {
return Math.PI * shape.radius [B] 2;
} else if ('sideLength' in shape) {
return shape.sideLength [/B] 2;
} else {
// Если в будущем тип Shape расширится новыми вариантами,
// TypeScript укажет на ошибку в этой строке
return assertNever(shape);
}
} |
|
Перечисления (enum)
Перечисления позволяют определить набор именованных констант, что делает код более читаемым и устойчивым к ошибкам:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| enum Direction {
North,
East,
South,
West
}
let currentDirection: Direction = Direction.North;
// По умолчанию значения начинаются с 0
console.log(Direction.North); // 0
console.log(Direction.East); // 1
// Можно задать начальное значение
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalServerError = 500
}
function handleResponse(status: HttpStatus) {
if (status === HttpStatus.OK) {
console.log("Запрос выполнен успешно");
}
} |
|
Строковые перечисления позволяют использовать строки вместо чисел, что часто более наглядно:
TypeScript | 1
2
3
4
5
6
7
8
| enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: Color = Color.Blue;
console.log(favoriteColor); // "BLUE" |
|
Объектные типы и интерфейсы
Для типизации объектов TypeScript предлагает два основных подхода: анонимные объектные типы и интерфейсы.
Анонимные объектные типы полезны для одноразового использования:
TypeScript | 1
2
3
4
5
6
7
8
9
10
| let user: { id: number; name: string; email?: string } = {
id: 1,
name: "Анна"
// email не обязателен благодаря модификатору "?"
};
// Функция, принимающая объект со свойствами x и y
function printPoint(point: { x: number; y: number }): void {
console.log(`X: ${point.x}, Y: ${point.y}`);
} |
|
Интерфейсы предпочтительны для повторного использования типов и более сложных структур:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| interface User {
id: number;
name: string;
email?: string;
readonly createdAt: Date;
updateProfile(newName: string): void;
}
let admin: User = {
id: 1,
name: "Администратор",
createdAt: new Date(),
updateProfile(newName) {
this.name = newName;
}
};
// Нельзя изменить свойство, помеченное как readonly
// admin.createdAt = new Date(); // Ошибка |
|
Тип object и Record
Тип object представляет любое нестроковое, нечисловое и небулевое значение:
TypeScript | 1
2
3
4
5
| let obj: object = { key: "value" };
// obj = 42; // Ошибка: Type 'number' is not assignable to type 'object'
// Однако доступ к свойствам ограничен
// console.log(obj.key); // Ошибка: Property 'key' does not exist on type 'object' |
|
Из-за этих ограничений обычно предпочтительнее использовать конкретные объектные типы или универсальные структуры вроде Record :
TypeScript | 1
2
3
4
5
6
7
8
9
| // Record<K, V> создаёт объект с ключами типа K и значениями типа V
let dictionary: Record<string, number> = {
"one": 1,
"two": 2,
"three": 3
};
// Можно добавлять новые пары ключ-значение
dictionary["four"] = 4; |
|
Типы bigint и symbol
TypeScript поддерживает новые примитивные типы JavaScript: bigint и symbol .
bigint используется для работы с целыми числами произвольной точности:
TypeScript | 1
2
3
| // Для использования bigint необходимо указать target ES2020 или выше в tsconfig.json
const bigNumber: bigint = 1234567890123456789012345678901234567890n;
const anotherBig: bigint = BigInt("9007199254740991"); |
|
symbol создаёт уникальные идентификаторы, часто используемые как ключи свойств:
TypeScript | 1
2
3
4
5
6
7
8
9
10
| const key1: symbol = Symbol("description");
const key2: symbol = Symbol("description");
console.log(key1 === key2); // false, символы всегда уникальны
const obj = {
[key1]: "Значение, доступное через символьный ключ"
};
console.log(obj[key1]); // "Значение, доступное через символьный ключ" |
|
Утверждения типов (Type Assertions)
Иногда разработчик лучше компилятора знает, какой тип имеет значение. В таких случаях можно использовать утверждения типов:
TypeScript | 1
2
3
4
5
6
7
8
| // Предположим, что getElementById возвращает HTMLElement или null
const input = document.getElementById("username") as HTMLInputElement;
// Теперь TypeScript "знает", что input — это HTMLInputElement
console.log(input.value);
// Альтернативный синтаксис (менее распространен)
const input2 = <HTMLInputElement>document.getElementById("password"); |
|
Утверждения не изменяют тип во время выполнения и не проводят преобразования — они только информируют компилятор о типе.
Немного об операторе typeof
TypeScript расширяет возможности оператора typeof для создания типов на основе значений:
TypeScript | 1
2
3
4
5
6
7
| const point = { x: 10, y: 20 };
type Point = typeof point; // { x: number; y: number; }
function greet(name: string, age: number) {
return `Привет, ${name}! Тебе ${age} лет.`;
}
type GreetFunction = typeof greet; // (name: string, age: number) => string |
|
Это особенно полезно при работе с существующими библиотеками или миграции кода с JavaScript на TypeScript.
понимание базовых типов TypeScript закладывает прочную основу для создания надёжного и поддерживаемого кода. Эти инструменты позволяют не только обнаруживать ошибки на этапе компиляции, но и делают код более читаемым и документированным, что критически важно для долгосрочных проектов и командной разработки.
Продвинутые типы
После освоения базовых типов TypeScript можно перейти к более сложным конструкциям, которые делают систему типов по-настоящему мощной. Продвинутые типы позволяют создавать гибкие абстракции и точно моделировать сложные отношения между данными.
Union и Intersection типы
Union-типы (объединения) позволяют указать, что значение может иметь один из нескольких типов. Они обозначаются с помощью вертикальной черты | :
TypeScript | 1
2
3
4
5
| // Переменная может содержать либо строку, либо число
let id: string | number;
id = "abc123"; // Корректно
id = 42; // Тоже корректно
id = true; // Ошибка: тип boolean не входит в union string | number |
|
Union-типы особенно полезны для функций, которые могут работать с разными типами данных:
TypeScript | 1
2
3
4
5
6
7
| function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase();
} else {
return `ID-${id.toString().padStart(6, '0')}`;
}
} |
|
Intersection-типы (пересечения) объединяют несколько типов в один, содержащий все свойства исходных типов. Они создаются с помощью амперсанда & :
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| type Employee = {
id: number;
name: string;
};
type Manager = {
employees: Employee[];
departmentId: number;
};
// Тип, объединяющий свойства Employee и Manager
type ManagerWithDetails = Employee & Manager;
const director: ManagerWithDetails = {
id: 1,
name: "Анна Иванова",
employees: [{id: 2, name: "Иван Петров"}],
departmentId: 100
}; |
|
Дженерики и их применение
Дженерики (обобщённые типы) — это способ создания компонентов, которые могут работать с различными типами, сохраняя при этом типобезопасность:
TypeScript | 1
2
3
4
5
6
7
8
9
| // Функция identity возвращает то же значение, что получила
function identity<T>(arg: T): T {
return arg;
}
// Явное указание типа
const numResult = identity<number>(42);
// Вывод типа
const strResult = identity("hello"); // TypeScript выведет T как string |
|
Дженерики часто используются при создании повторно используемых компонентов и контейнеров:
TypeScript | 1
2
3
4
5
6
7
8
| // Обобщённый интерфейс для контейнера
interface Box<T> {
value: T;
}
// Использование с конкретным типом
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" }; |
|
Дженерики позволяют задавать ограничения на типы параметров:
TypeScript | 1
2
3
4
5
6
7
8
9
| // T должен иметь свойство length
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength("string"); // Работает: у строк есть свойство length
getLength([1, 2, 3]); // Работает: у массивов есть свойство length
getLength({ length: 5 }); // Работает: у объекта есть свойство length
// getLength(42); // Ошибка: у чисел нет свойства length |
|
Типы-утилиты
TypeScript предоставляет набор встроенных типов-утилит для преобразования существующих типов:
Partial<T> — делает все свойства типа T необязательными:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| interface User {
id: number;
name: string;
email: string;
}
// Все свойства становятся необязательными
function updateUser(userId: number, updates: Partial<User>) {
// Можно передать только нужные поля
}
updateUser(1, { name: "Новое имя" }); // Не требуется email |
|
Required<T> — противоположность Partial, делает все свойства обязательными:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| interface Config {
host?: string;
port?: number;
secure?: boolean;
}
// Все свойства становятся обязательными
const fullConfig: Required<Config> = {
host: "localhost",
port: 8080,
secure: true
}; |
|
Readonly<T> — делает все свойства доступными только для чтения:
TypeScript | 1
2
3
4
5
6
7
| const frozenUser: Readonly<User> = {
id: 1,
name: "Иван",
email: "ivan@example.com"
};
// frozenUser.name = "Петр"; // Ошибка: нельзя изменить readonly свойство |
|
Pick<T, K> — создаёт тип из подмножества свойств T:
TypeScript | 1
2
3
4
5
6
7
8
| // Только id и name из интерфейса User
type BasicUser = Pick<User, "id" | "name">;
const basicUser: BasicUser = {
id: 1,
name: "Иван"
// email не нужен
}; |
|
Omit<T, K> — создаёт тип, исключая указанные свойства:
TypeScript | 1
2
3
4
5
6
7
8
| // Все свойства User кроме email
type UserWithoutEmail = Omit<User, "email">;
const userNoEmail: UserWithoutEmail = {
id: 1,
name: "Иван"
// email не разрешен
}; |
|
Record<K, T> — создаёт тип с ключами типа K и значениями типа T:
TypeScript | 1
2
3
4
5
6
| // Объект с числовыми ключами и строковыми значениями
const ages: Record<number, string> = {
10: "десять",
20: "двадцать",
30: "тридцать"
}; |
|
Conditional Types и mapped types для создания гибких систем
Условные типы (Conditional Types) позволяют выбрать тип на основе условия:
TypeScript | 1
2
3
4
5
6
| // T extends U ? X : Y — если T является подтипом U, выбираем X, иначе Y
type IsString<T> = T extends string ? true : false;
// Примеры использования
type A = IsString<"hello">; // true
type B = IsString<42>; // false |
|
Условные типы часто используются вместе с дженериками для создания гибких и адаптивных систем типов:
TypeScript | 1
2
3
4
5
6
7
8
| // Извлекаем тип возвращаемого значения функции
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchUser() {
return { id: 1, name: "Анна" };
}
type User = ReturnType<typeof fetchUser>; // { id: number; name: string; } |
|
Mapped Types (преобразованные типы) позволяют трансформировать каждое свойство исходного типа:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // Делаем все свойства объекта необязательными
type Optional<T> = {
[P in keyof T]?: T[P];
};
interface Person {
name: string;
age: number;
}
// Все свойства стали необязательными
const partialPerson: Optional<Person> = { name: "Иван" }; |
|
Можно комбинировать mapped types с другими конструкциями:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // Делаем все свойства доступными только для чтения и строковыми
type ReadonlyStringified<T> = {
readonly [P in keyof T]: string;
};
const stringPerson: ReadonlyStringified<Person> = {
name: "Иван",
age: "30" // Теперь это строка
};
// stringPerson.name = "Петр"; // Ошибка: нельзя изменить readonly свойство |
|
Продвинутые типы в TypeScript предоставляют богатый инструментарий для моделирования сложной бизнес-логики и создания гибких, типобезопасных API. Они помогают предотвращать потенциальные ошибки и сделать код более самодокументированным, что особенно ценно в крупных проектах с множеством разработчиков. TypeScript продолжает эволюционировать, и новые версии регулярно добавляют мощные возможности к системе типов, позволяя ещё точнее моделировать отношения между данными и упрощая разработку сложных приложений.
Type Guards и сужение типов (Type Narrowing)
Type Guards (защитники типов) — это конструкции, позволяющие TypeScript сужать тип переменной в определённом блоке кода. Они особенно полезны при работе с union-типами, когда нужно выполнить определённые операции в зависимости от конкретного типа значения. Базовые защитники типов используют оператор typeof :
TypeScript | 1
2
3
4
5
6
7
8
9
| function processValue(value: string | number) {
// В этом блоке TypeScript знает, что value — строка
if (typeof value === "string") {
return value.toUpperCase();
}
// А в этом блоке — число
return value.toFixed(2);
} |
|
Для проверки свойств объектов можно использовать оператор in :
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
// TypeScript знает, что здесь animal — это Bird
animal.fly();
} else {
// А здесь — Fish
animal.swim();
}
} |
|
Для классов работает оператор instanceof :
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Car {
drive() { console.log("Вррум!"); }
}
class Bicycle {
ride() { console.log("Крутим педали!"); }
}
function useVehicle(vehicle: Car | Bicycle) {
if (vehicle instanceof Car) {
vehicle.drive();
} else {
vehicle.ride();
}
} |
|
Пользовательские защитники типов
Иногда встроенных защитников недостаточно. В таких случаях можно создать функцию-предикат, возвращающую булево значение и имеющую специальную сигнатуру parameterName is Type :
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Rectangle | Circle;
// Пользовательский защитник типа
function isRectangle(shape: Shape): shape is Rectangle {
return shape.kind === "rectangle";
}
function calculateArea(shape: Shape): number {
if (isRectangle(shape)) {
// TypeScript знает, что shape — это Rectangle
return shape.width * shape.height;
} else {
// А здесь shape — это Circle
return Math.PI * shape.radius ** 2;
}
} |
|
Дискриминантные объединения (Discriminated Unions)
Часто для union-типов используется общее свойство-дискриминатор (например, kind или type ), которое позволяет однозначно определить конкретный тип:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
case "circle":
return Math.PI * shape.radius ** 2;
default:
// TypeScript проверит, что все варианты обработаны
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
} |
|
Обратите внимание на последнюю строку с _exhaustiveCheck . Это техника, которая гарантирует, что все возможные типы обработаны: если в будущем тип Shape будет расширен, но обработчик не обновлен, компилятор выдаст ошибку.
Типизация классов и интерфейсов: различия и особенности
В TypeScript интерфейсы и классы тесно связаны, но имеют различные цели и особенности применения.
Интерфейсы для описания структуры
Интерфейсы определяют структуру объекта, не предоставляя реализации:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| interface Drawable {
draw(): void;
resize(width: number, height: number): void;
}
// Класс, реализующий интерфейс
class Circle implements Drawable {
constructor(private radius: number) {}
draw() {
console.log(`Рисуем круг радиусом ${this.radius}`);
}
resize(width: number, height: number) {
this.radius = Math.min(width, height) / 2;
}
} |
|
Интерфейсы могут расширять друг друга, создавая более сложные структуры:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: string;
department: string;
}
// Объект должен содержать все поля из обоих интерфейсов
const manager: Employee = {
name: "Иван",
age: 35,
employeeId: "EMP123",
department: "IT"
}; |
|
Классы: объединение данных и поведения
Классы объединяют структуру данных и логику поведения:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class Animal {
// Модификаторы доступа: public, private, protected
constructor(protected name: string) {}
move(distance: number = 0): void {
console.log(`${this.name} передвигается на ${distance}м.`);
}
}
class Dog extends Animal {
constructor(name: string, private breed: string) {
super(name);
}
bark(): void {
console.log("Гав-гав!");
}
// Переопределение метода родительского класса
move(distance: number = 5): void {
console.log(`${this.name} породы ${this.breed} бежит...`);
super.move(distance);
}
}
const dog = new Dog("Рекс", "Овчарка");
dog.bark();
dog.move(10); |
|
Абстрактные классы
Абстрактные классы занимают промежуточное положение между интерфейсами и обычными классами. Они могут содержать как абстрактные методы (без реализации), так и конкретные:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| abstract class Shape {
constructor(protected color: string) {}
// Абстрактный метод, должен быть реализован в подклассах
abstract calculateArea(): number;
// Конкретный метод с реализацией
display(): void {
console.log(`Фигура цвета ${this.color}`);
}
}
class Rectangle extends Shape {
constructor(
color: string,
private width: number,
private height: number
) {
super(color);
}
calculateArea(): number {
return this.width * this.height;
}
// Дополнительный метод, специфичный для прямоугольника
get diagonal(): number {
return Math.sqrt(this.width [B] 2 + this.height [/B] 2);
}
} |
|
Основные различия между классами и интерфейсами
1. Реализация: интерфейсы не содержат реализации, классы могут содержать как объявления, так и реализацию.
2. Экземпляры: нельзя создать экземпляр интерфейса, можно создать экземпляр класса.
3. Расширение: класс может реализовать множество интерфейсов, но расширить только один класс.
4. Видимость: классы поддерживают модификаторы доступа (public, private, protected), интерфейсы — нет.
5. Слияние деклараций: несколько интерфейсов с одинаковым именем автоматически объединяются, с классами такого не происходит.
Приватные и защищенные поля
TypeScript расширяет JavaScript, добавляя концепции приватных и защищенных полей:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| class BankAccount {
// Публичное свойство, доступно везде
public owner: string;
// Приватное свойство, доступно только внутри класса
private balance: number;
// Защищенное свойство, доступно в классе и подклассах
protected accountNumber: string;
constructor(owner: string, initialBalance: number) {
this.owner = owner;
this.balance = initialBalance;
this.accountNumber = `ACC${Math.floor(Math.random() * 10000)}`;
}
deposit(amount: number): void {
this.balance += amount;
}
getBalance(): number {
return this.balance;
}
}
class SavingsAccount extends BankAccount {
private interestRate: number;
constructor(owner: string, initialBalance: number, interestRate: number) {
super(owner, initialBalance);
this.interestRate = interestRate;
}
addInterest(): void {
// Можем использовать accountNumber (protected)
console.log(`Начисляем проценты на счет ${this.accountNumber}`);
// Но не можем напрямую обратиться к balance (private)
// Используем публичный метод
const currentBalance = this.getBalance();
this.deposit(currentBalance * this.interestRate);
}
} |
|
В TypeScript 3.8+ также поддерживаются приватные поля ECMAScript, которые обеспечивают истинную приватность на уровне языка JavaScript:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Person {
#secretId: string;
constructor(public name: string) {
this.#secretId = `secret-${Math.random()}`;
}
getSecretId(): string {
return this.#secretId;
}
}
const person = new Person("Анна");
console.log(person.name); // OK
// console.log(person.#secretId); // Ошибка: приватное поле |
|
Namespace и модули: организация типов в больших проектах
Для организации кода в крупных проектах TypeScript предлагает два механизма: пространства имен (namespaces) и модули (modules).
Пространства имен
Пространства имен группируют связанные функциональности и помогают избежать конфликтов имен:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| namespace Geometry {
export interface Point {
x: number;
y: number;
}
export class Line {
constructor(public start: Point, public end: Point) {}
length(): number {
const dx = this.end.x - this.start.x;
const dy = this.end.y - this.start.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
// Вложенное пространство имен
export namespace ThreeDimensional {
export interface Point3D extends Point {
z: number;
}
}
}
// Использование
const point1: Geometry.Point = { x: 0, y: 0 };
const point2: Geometry.Point = { x: 3, y: 4 };
const line = new Geometry.Line(point1, point2);
console.log(line.length()); // 5
// Использование вложенного пространства имен
const point3D: Geometry.ThreeDimensional.Point3D = { x: 1, y: 2, z: 3 }; |
|
Модули в TypeScript
В отличие от пространств имен, модули существуют на уровне файлов и предназначены для работы с современными системами модулей JavaScript (ES Modules, CommonJS):
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export interface MathOperation {
(a: number, b: number): number;
} |
|
TypeScript | 1
2
3
4
5
6
7
8
9
10
| // app.ts
import { add, MathOperation } from './math';
// Импорт всего модуля
import * as Math from './math';
const sum = add(5, 3);
const difference = Math.subtract(10, 4);
// Использование импортированного интерфейса
const multiply: MathOperation = (a, b) => a * b; |
|
В современной разработке модули считаются предпочтительным способом организации кода, в то время как пространства имен используются в основном в старых кодовых базах или для внутренних структур.
Индексные типы и операторы доступа к ключам
TypeScript предоставляет операторы для работы с ключами типов и их значениями.
Оператор keyof
Оператор keyof создает объединение строковых литералов из ключей объектного типа:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| interface Person {
name: string;
age: number;
address: string;
}
// 'name' | 'age' | 'address'
type PersonKeys = keyof Person;
// Функция для безопасного получения свойства объекта по ключу
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const john: Person = {
name: "Иван",
age: 30,
address: "ул. Примерная, 123"
};
const johnName = getProperty(john, "name"); // строка
const johnAge = getProperty(john, "age"); // число
// const error = getProperty(john, "job"); // Ошибка: "job" не является ключом Person |
|
Индексные типы доступа
Оператор T[K] позволяет получить тип значения по ключу:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| interface Dictionary<T> {
[key: string]: T;
}
const stringDict: Dictionary<string> = {
"a": "apple",
"b": "banana"
};
// Получаем тип элементов словаря: string
type DictValue = Dictionary<string>["a"];
// С ограниченным набором ключей
interface PersonWithCategories {
name: string;
age: number;
categories: {
primary: string;
secondary: string;
};
}
// Тип для категорий: { primary: string; secondary: string; }
type Categories = PersonWithCategories["categories"];
// Тип для первичной категории: string
type PrimaryCategory = PersonWithCategories["categories"]["primary"]; |
|
Строковые литералы и шаблонные литеральные типы
TypeScript 4.1 и выше поддерживает шаблонные литеральные типы, которые работают аналогично шаблонным строкам, но на уровне типов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| type Color = "red" | "green" | "blue";
type Quantity = "one" | "two" | "three";
// "one_red" | "one_green" | "one_blue" | "two_red" | ... и т.д.
type ColorQuantity = `${Quantity}_${Color}`;
// Преобразование ключей в camelCase
type CamelCase<S extends string> = S extends `${infer P}_${infer Q}`
? `${P}${Capitalize<Q>}`
: S;
// Пример использования: "user_id" -> "userId"
type CamelCaseTest = CamelCase<"user_id">;
// Преобразование всех ключей объекта
type CamelCaseKeys<T> = {
[K in keyof T as CamelCase<string & K>]: T[K]
};
interface ApiResponse {
user_id: number;
first_name: string;
last_name: string;
}
// Результат: { userId: number; firstName: string; lastName: string; }
type NiceResponse = CamelCaseKeys<ApiResponse>; |
|
Рекурсивные типы
Типы в TypeScript могут быть рекурсивными, что полезно для представления древовидных структур или вложенных данных:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // Рекурсивный тип для древовидной структуры
type TreeNode<T> = {
value: T;
children?: TreeNode<T>[];
};
// Использование
const tree: TreeNode<string> = {
value: "root",
children: [
{ value: "child1" },
{
value: "child2",
children: [
{ value: "grandchild1" },
{ value: "grandchild2" }
]
}
]
};
// Рекурсивный тип для JSON-подобных данных
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const data: JSONValue = {
name: "Документ",
created: true,
value: 42,
metadata: {
author: "Иван",
tags: ["важное", "документация"]
}
}; |
|
Декораторы и метаданные
Декораторы — экспериментальная возможность TypeScript, которая позволяет добавлять аннотации и метапрограммирование к классам и их членам. Они популярны в фреймворках вроде Angular и NestJS:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // Включите "experimentalDecorators": true в tsconfig.json
// Декоратор класса
function Logger(target: Function) {
console.log(`Создан класс: ${target.name}`);
}
// Декоратор свойства
function Property(target: any, propertyKey: string) {
console.log(`Определено свойство: ${propertyKey}`);
}
// Декоратор метода
function Method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Вызов ${propertyKey} с аргументами: ${JSON.stringify(args)}`);
return original.apply(this, args);
};
}
@Logger
class Example {
@Property
name: string;
constructor(name: string) {
this.name = name;
}
@Method
greet(message: string): string {
return `${this.name} говорит: ${message}`;
}
}
const example = new Example("Анна");
console.log(example.greet("Привет!"));
// Вывод:
// Создан класс: Example
// Определено свойство: name
// Вызов greet с аргументами: ["Привет!"]
// Анна говорит: Привет! |
|
Типы расширения миксинов (Mixin Types)
Миксины позволяют создавать классы, которые комбинируют функциональность из нескольких источников:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // Базовый класс
class Base {
isBase = true;
baseMethod() { return "base method"; }
}
// Миксин, добавляющий функциональность таймштампов
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
getTimestamp() {
return new Date(this.timestamp);
}
};
}
// Миксин для идентификации
function Identified<TBase extends Constructor>(Base: TBase) {
return class extends Base {
id = Math.random().toString(36).substring(2, 9);
getId() {
return this.id;
}
};
}
// Объединение миксинов
const TimestampedBase = Timestamped(Base);
const IdentifiedTimestampedBase = Identified(TimestampedBase);
// Использование комбинированного класса
const instance = new IdentifiedTimestampedBase();
console.log(instance.isBase); // true
console.log(instance.baseMethod()); // "base method"
console.log(instance.getTimestamp()); // текущая дата
console.log(instance.getId()); // случайный ID |
|
Продвинутые типы TypeScript позволяют моделировать сложные зависимости и отношения между данными, которые невозможно выразить в обычном JavaScript. Они повышают безопасность и читаемость кода, что особенно важно для крупных проектов и коллективной разработки.
Типизация в реальных проектах
Применение TypeScript в реальных проектах существенно отличается от академического изучения типов. Здесь мы сталкиваемся с ограничениями существующего кода, техническим долгом, производительностью и необходимостью интеграции с нетипизированными библиотеками. Давайте рассмотрим, как эффективно использовать типизацию в боевых условиях.
Частые ошибки и их решения
При работе с TypeScript разработчики часто сталкиваются с типичными проблемами, которые имеют проверенные решения.
Злоупотребление типом any
Одна из самых распространённых ошибок — чрезмерное использование типа any . Этот тип отключает проверку и может создать ложное чувство безопасности:
TypeScript | 1
2
3
4
| // Анти-паттерн
function processData(data: any) {
return data.someProperty.method(); // Может взорваться в рантайме
} |
|
Решение: Используйте unknown как более безопасную альтернативу, требующую явной проверки типа:
TypeScript | 1
2
3
4
5
6
7
| function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'someProperty' in data) {
const item = data as { someProperty: { method: () => void } };
return item.someProperty.method();
}
throw new Error('Некорректный формат данных');
} |
|
Неправильное использование утверждений типа
Частое применение оператора as для утверждения типов может нивелировать преимущества статической типизации:
TypeScript | 1
2
| // Анти-паттерн
const userData = JSON.parse(response) as UserData; // Опасно! |
|
Решение: Валидируйте данные перед преобразованием типа:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| function isUserData(data: unknown): data is UserData {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data &&
typeof (data as any).id === 'number' &&
typeof (data as any).name === 'string'
);
}
const responseData = JSON.parse(response);
if (isUserData(responseData)) {
const userData: UserData = responseData;
// Безопасно использовать userData
} else {
throw new Error('Некорректный формат пользовательских данных');
} |
|
Забывание про обработку null и undefined
Многие ошибки возникают из-за неучтённых краевых случаев с null и undefined :
TypeScript | 1
2
3
4
| // Анти-паттерн
function getFirstItem(items: string[]) {
return items[0].toUpperCase(); // Может вызвать ошибку, если массив пуст
} |
|
Решение: Используйте строгую проверку null и защитные проверки:
TypeScript | 1
2
3
4
5
6
7
8
9
| function getFirstItem(items: string[]): string | undefined {
const item = items[0];
return item ? item.toUpperCase() : undefined;
}
// Или с оператором опциональной последовательности
function getFirstItem(items: string[]): string | undefined {
return items[0]?.toUpperCase();
} |
|
Оптимизация работы с типами
Правильная организация типов приводит к более производительному и поддерживаемому коду.
Используйте файлы определений типов
Размещайте общие типы в отдельных файлах для улучшения модульности и переиспользования:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // types.ts
export interface User {
id: number;
name: string;
email: string;
}
export type UserRole = 'admin' | 'moderator' | 'user';
export interface AuthResponse {
user: User;
token: string;
role: UserRole;
} |
|
Избегайте дублирования типов
Дублирование типов создаёт проблемы при изменении структуры данных. Вместо этого используйте композицию и наследование типов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Анти-паттерн
interface User {
id: number;
name: string;
email: string;
}
interface UserWithRole { // Дублирование полей
id: number;
name: string;
email: string;
role: string;
}
// Правильный подход
interface User {
id: number;
name: string;
email: string;
}
interface UserWithRole extends User {
role: string;
} |
|
Используйте утилитные типы для упрощения сложных структур
TypeScript предоставляет мощные утилитные типы, которые избавляют от необходимости создавать собственные преобразования:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// Получение только полезных данных
type ResponseData<T> = ApiResponse<T>['data'];
// Частичные обновления
interface User {
id: number;
name: string;
email: string;
settings: {
theme: string;
notifications: boolean;
};
}
// Тип для обновления только настроек
type UserSettingsUpdate = Partial<Pick<User, 'settings'>>;
// Использование
function updateUserSettings(userId: number, settings: UserSettingsUpdate) {
// Реализация обновления
} |
|
Инструменты и советы для эффективной типизации
Экосистема TypeScript включает множество инструментов, упрощающих разработку.
Настройка tsconfig.json
Корректная настройка компилятора критически важна для эффективной работы:
JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| {
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
} |
|
Включение строгих проверок (strict: true ) поможет выявить потенциальные проблемы на ранних этапах.
Линтеры и форматирование
ESLint с плагином TypeScript обеспечивает единообразие кода и выявляет типичные проблемы:
JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error"
}
} |
|
Автоматическая генерация типов
Для API и GraphQL можно использовать инструменты автоматической генерации типов из схемы:
Bash | 1
2
3
4
5
| # Для REST API из JSON или OpenAPI
npx openapi-typescript https://api.example.com/swagger.json -o src/types/api.ts
# Для GraphQL
npx graphql-codegen --config codegen.yml |
|
Стратегии миграции JavaScript-проектов на TypeScript
Миграция существующего JavaScript-проекта на TypeScript — процесс, требующий планирования.
Постепенный подход
Наиболее безопасная стратегия — постепенная миграция:
1. Настройте проект для сосуществования JS и TS:
JSON | 1
2
3
4
5
6
7
8
9
10
| // tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"outDir": "dist",
"strict": false // Начните с менее строгих настроек
},
"include": ["src/**/*.ts", "src/**/*.js"]
} |
|
2. Переименуйте файлы .js в .ts без изменения кода.
3. Добавляйте типы постепенно, начиная с any где необходимо:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // До миграции (js)
function calculateTotal(items) {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}
// Начальная миграция (ts)
function calculateTotal(items: any[]): number {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}
// Полная типизация
interface CartItem {
id: string;
price: number;
quantity: number;
}
function calculateTotal(items: CartItem[]): number {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
} |
|
4. Включайте строгие проверки постепенно:
JSON | 1
2
3
4
5
6
| {
"compilerOptions": {
"noImplicitAny": true,
// Другие строгие опции добавляйте по мере готовности
}
} |
|
Использование JSDoc для переходного периода
JSDoc-комментарии позволяют добавлять типизацию в JavaScript перед полной миграцией:
JavaScript | 1
2
3
4
5
6
7
8
| /**
* Рассчитывает общую стоимость заказа.
* @param {Array<{id: string, price: number, quantity: number}>} items Элементы заказа
* @returns {number} Общая стоимость
*/
function calculateTotal(items) {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
} |
|
TypeScript распознает JSDoc-аннотации, что позволяет получить преимущества типизации без изменения файлов.
Производительность и оптимизация компилятора TypeScript
При работе с крупными проектами производительность компилятора TypeScript может стать узким местом в процессе разработки. Замедление сборки и задержки при проверке типов снижают эффективность разработчиков.
Оптимизация настроек компилятора
Некоторые настройки в tsconfig.json напрямую влияют на скорость компиляции:
JSON | 1
2
3
4
5
6
7
8
| {
"compilerOptions": {
"incremental": true,
"skipLibCheck": true,
"sourceMap": false,
"isolatedModules": true
}
} |
|
incremental: true включает инкрементальную компиляцию, сохраняя информацию о предыдущей компиляции.
skipLibCheck: true пропускает проверку типов в файлах определений (.d.ts).
sourceMap: false отключает генерацию source maps в процессе разработки, если они не нужны.
isolatedModules: true заставляет писать код в модульном стиле что ускоряет параллельную компиляцию.
Разделение кода на модули
Разбиение кода на небольшие модули повышает производительность благодаря возможностям параллельной компиляции:
TypeScript | 1
2
3
4
5
6
7
8
| // Вместо одного большого файла
// components.ts
// Разделите на несколько маленьких
// components/Button.ts
// components/Input.ts
// components/Card.ts
// components/index.ts (реэкспорт) |
|
Избегайте сложных типов
Сложные условные и распределённые типы могут значительно замедлить проверку типов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Медленный тип с глубокой вложенностью
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P]
};
// Более эффективная альтернатива для больших объектов
interface ReadonlyUser {
readonly id: number;
readonly name: string;
readonly settings: ReadonlySettings;
} |
|
Типизация асинхронного кода и Promise
Асинхронное программирование — основа современной веб-разработки, и TypeScript предоставляет инструменты для типизации асинхронного кода.
Базовая типизация Promise
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Возвращает Promise с конкретным типом результата
async function fetchUserData(id: number): Promise<UserData> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data as UserData;
}
// Использование
async function displayUser(id: number): Promise<void> {
try {
const user = await fetchUserData(id);
console.log(`Загружен пользователь: ${user.name}`);
} catch (error) {
console.error("Ошибка загрузки:", error);
}
} |
|
Обработка ошибок в типизированном асинхронном коде
В TypeScript 4.0+ появилась возможность типизировать исключения с помощью защитников типа:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| class NetworkError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(public fields: string[], message: string) {
super(message);
this.name = "ValidationError";
}
}
async function fetchData(): Promise<UserData> {
try {
// реализация
} catch (error: unknown) {
if (error instanceof NetworkError) {
// Обработка сетевой ошибки
if (error.statusCode === 404) {
return getDefaultUser(); // Возвращаем значение по умолчанию
}
}
if (error instanceof ValidationError) {
// Обработка ошибки валидации
console.error("Некорректные поля:", error.fields);
}
throw error; // Пробрасываем остальные ошибки
}
} |
|
Композиция асинхронных операций
При объединении нескольких асинхронных вызовов сохраняйте типобезопасность:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| interface User {
id: number;
name: string;
}
interface Post {
id: number;
title: string;
content: string;
}
// Получение связанных данных
async function getUserWithPosts(userId: number): Promise<{user: User; posts: Post[]}> {
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId)
]);
return { user, posts };
} |
|
Типизация сторонних библиотек и работа с .d.ts файлами
При использовании сторонних библиотек, особенно написанных на JavaScript, TypeScript требует определений типов.
Использование DefinitelyTyped
Большинство популярных библиотек имеют определения типов в репозитории DefinitelyTyped:
Bash | 1
2
| # Установка типов для библиотеки lodash
npm install --save-dev @types/lodash |
|
Создание собственных определений типов
Если библиотека не имеет готовых типов, создайте файл определений:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // types/untyped-library/index.d.ts
declare module 'untyped-library' {
export function doSomething(input: string): number;
export interface Options {
timeout?: number;
retries?: number;
}
export default function main(options?: Options): void;
} |
|
Не забудьте добавить путь к определениям в tsconfig.json :
JSON | 1
2
3
4
5
| {
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
} |
|
Использование модуля declaration merging
Для существующих библиотек с неполными типами можно дополнить определения:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Расширение существующего модуля
import { Express } from 'express';
// Добавление пользовательских свойств
declare global {
namespace Express {
interface Request {
user?: {
id: string;
role: string;
};
}
}
} |
|
Типизация React-компонентов и Redux-состояния
TypeScript особенно полезен при работе с популярными фреймворками.
Типизация функциональных компонентов React
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| interface ButtonProps {
text: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
// Используем React.FC с дженериком (хотя современный React склоняется к явному указанию типа возврата)
const Button: React.FC<ButtonProps> = ({ text, onClick, variant = 'primary', disabled = false }) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
};
// Более современный подход
function Button({ text, onClick, variant = 'primary', disabled = false }: ButtonProps): JSX.Element {
// Реализация
} |
|
Типизация состояния Redux
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Определение типов действий
type ActionTypes =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'TOGGLE_TODO'; payload: { id: number } }
| { type: 'REMOVE_TODO'; payload: { id: number } };
// Определение типа состояния
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
loading: boolean;
}
// Типизированный редьюсер
function todoReducer(state: TodoState, action: ActionTypes): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }]
};
// Другие случаи
default:
return state;
}
} |
|
CI/CD и автоматическая проверка типов в процессе разработки
Интеграция проверки типов в процесс непрерывной интеграции обеспечивает надежность кодовой базы.
Настройка проверки типов в CI-пайплайне
YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # .github/workflows/typescript-check.yml
name: TypeScript Check
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: TypeScript check
run: npx tsc --noEmit |
|
Автоматическая генерация отчетов о типах
Для анализа покрытия типами можно настроить инструменты вроде type-coverage:
Bash | 1
2
3
4
| npm install --save-dev type-coverage
# Проверка покрытия типами
npx type-coverage --detail |
|
Интеграция TypeScript в реальные проекты требует не только знания синтаксиса, но и понимания практических аспектов, включая производительность, интеграцию и процессы разработки. Благодаря правильному подходу система типов становится не препятствием, а мощным инструментом, ускоряющим разработку и повышающим качество кода.
Типизация в реальных проектах: практические аспекты
Продолжая разговор о типизации в реальных проектах, нельзя не затронуть ряд важных аспектов, с которыми сталкиваются разработчики при внедрении TypeScript в производственные системы. Прежде всего, это вопросы масштабирования и поддержки типов в долгосрочной перспективе.
Организация типов в крупных проектах
В больших командах и сложных проектах организация типов становится критически важной для поддержания кодовой базы. Существует несколько проверенных временем подходов.
Монорепозитории и типизация между пакетами
Монорепозитории — популярное решение для крупных проектов, объединяющих несколько взаимосвязанных пакетов. TypeScript хорошо вписывается в такую архитектуру, но требует дополнительных настроек:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // packages/common/src/types/index.ts
export interface User {
id: string;
name: string;
email: string;
}
// packages/api/src/users/service.ts
import { User } from '@project/common';
export async function getUserById(id: string): Promise<User> {
// Реализация
}
// packages/frontend/src/components/UserProfile.tsx
import { User } from '@project/common';
import { getUserById } from '@project/api';
interface Props {
userId: string;
}
export function UserProfile({ userId }: Props) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
async function fetchUser() {
try {
const userData = await getUserById(userId);
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
}
}
fetchUser();
}, [userId]);
// Отображение профиля
} |
|
Для эфективной работы монорепозитория с TypeScript рекомендуется:
1. Настроить ссылки на проект (project references) в tsconfig.json:
JSON | 1
2
3
4
5
6
7
8
9
10
11
| // packages/api/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist"
},
"references": [
{ "path": "../common" }
]
} |
|
2. Использовать инструменты для монорепозиториев, такие как Lerna, Nx или Turborepo, которые поддерживают компиляцию TypeScript с учётом зависимостей между пакетами.
Подход к совместному использованию типов
Существует три основных подхода к совместному использованию типов между частями приложения:
1. Централизованный подход — все типы определяются в едином месте:
TypeScript | 1
2
3
4
5
6
| src/
├── types/
│ ├── index.ts # Реэкспорт всех типов
│ ├── user.ts # Типы, связанные с пользователями
│ ├── product.ts # Типы, связанные с продуктами
│ └── ... |
|
2. Распределённый подход — типы определяются рядом с их использованием и экспортируются при необходимости:
TypeScript | 1
2
3
4
5
6
7
8
9
| src/
├── features/
│ ├── user/
│ │ ├── types.ts # Типы пользователя
│ │ ├── api.ts
│ │ └── components/
│ └── product/
│ ├── types.ts # Типы продукта
│ └── ... |
|
3. Гибридный подход — общие типы централизованы, специфичные типы распределены:
TypeScript | 1
2
3
4
5
6
7
8
| src/
├── types/ # Общие типы
│ ├── common.ts
│ └── api.ts
├── features/
│ ├── user/
│ │ ├── types.ts # Специфичные типы пользователя
│ │ └── ... |
|
Выбор подхода зависит от размера команды, сложности проекта и предпочтений разработчиков. В крупных проектах гибридный подход часто оказывается наиболее эфективным.
Взаимодействие с бэкендом и типизация API
Одна из самых сложных задач в реальных приложениях — обеспечение согласованности типов между фронтендом и бэкендом. Существует несколько стратегий решения этой проблемы.
Генерация типов из схемы API
Для REST API можно генерировать типы из OpenAPI (Swagger) спецификации:
Bash | 1
2
3
4
5
| # Установка инструмента для генерации
npm install --save-dev openapi-typescript
# Генерация типов из спецификации
npx openapi-typescript https://api.example.com/swagger.json -o src/types/api.ts |
|
Получется набор типов, которые можно использовать при работе с API:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import type { components } from '../types/api';
type User = components['schemas']['User'];
type CreateUserRequest = components['schemas']['CreateUserRequest'];
async function createUser(data: CreateUserRequest): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json() as User;
} |
|
Совместное использование типов между фронтендом и бэкендом
Если и фронтенд, и бэкенд написаны на TypeScript, можно создать общий пакет с типами:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // packages/shared-types/index.ts
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
createdAt: string; // ISO date string
}
export interface CreateUserDto {
email: string;
name: string;
password: string;
}
// Типы ответов API
export interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
message?: string;
} |
|
Этот подход обеспечивает согласованность типов на всех уровнях приложения и позволяет избежать дублирования кода.
Эволюция типов и обратная совместимость
В реальных проектах типы, как и сам код, развиваются со временем. Управление изменениями типов требует особого внимания, особенно в публичных API и библиотеках.
Семантическое версионирование типов
При изменении типов в публичном API следует придерживаться принципов семантического версионирования:
Патч-версия (1.0.x): исправления, не меняющие API (улучшение документации типов, более точные типы без нарушения существующего кода).
Минорная версия (1.x.0): добавление новых возможностей без нарушения обратной совместимости (новые свойства с модификатором ? , новые функции).
Мажорная версия (x.0.0): изменения, нарушающие обратную совместимость (удаление свойств, изменение сигнатур функций).
Поддержка обратной совместимости
Существует несколько техник для поддержки обратной совместимости при изменении типов:
1. Опциональные свойства для новых полей:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // Было
interface User {
id: string;
name: string;
}
// Стало (обратно совместимо)
interface User {
id: string;
name: string;
email?: string; // Новое опциональное поле
} |
|
2. Перегрузка функций для изменения сигнатуры:
TypeScript | 1
2
3
4
5
6
7
8
9
| // Было
function fetchUsers(query: string): Promise<User[]>;
// Стало (обратно совместимо)
function fetchUsers(query: string): Promise<User[]>;
function fetchUsers(query: string, options: FetchOptions): Promise<User[]>;
function fetchUsers(query: string, options?: FetchOptions): Promise<User[]> {
// Реализация
} |
|
3. Объединение типов для поддержки старых и новых версий:
TypeScript | 1
2
3
4
5
6
7
| // Было
type UserRole = 'admin' | 'user';
// Стало (обратно совместимо)
type LegacyUserRole = 'admin' | 'user';
type NewUserRole = 'admin' | 'user' | 'moderator';
type UserRole = LegacyUserRole | NewUserRole; |
|
Маркировка устаревших типов
Для обозначения устаревших типов, которые планируется удалить в будущем, можно использовать JSDoc-комментарии:
TypeScript | 1
2
3
4
5
6
7
8
9
10
| /**
* @deprecated Use `NewUserInterface` instead. Will be removed in version 3.0.0.
*/
export interface User {
// ...
}
export interface NewUserInterface {
// ...
} |
|
Современные IDE показывают предупреждения при использовании помеченных как устаревшие типов, что помогает разработчикам постепенно мигрировать на новые версии.
Тестирование типов
Проверка корректности типов — не менее важная часть процесса разработки, чем юнит-тестирование функциональности.
Статические тесты типов
TypeScript позволяет создавать статические тесты для проверки правильности определения типов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // tests/types.test.ts
import { expectType } from 'tsd';
import { User, createUser } from '../src/user';
// Проверка, что функция возвращает правильный тип
expectType<Promise<User>>(createUser('John', 'john@example.com'));
// Проверка, что тип содержит ожидаемые свойства
type UserKeys = keyof User;
expectType<'id' | 'name' | 'email'>('' as unknown as UserKeys);
// Проверка, что тип не допускает неверные значения
// @ts-expect-error
const invalidUser: User = { name: 'John' }; // Отсутствует обязательное поле id |
|
Эти тесты не выполняются во время выполнения, а проверяются компилятором TypeScript. Они помогают убедиться, что интерфейсы и типы работают так, как ожидается.
Проверка типобезопасности в CI/CD
Интеграция проверки типов в процесс непрерывной интеграции гарантирует, что все изменения кода соответствуют определённым типам:
YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # .github/workflows/type-check.yml
name: Type Check
on:
push:
branches: [ main, development ]
pull_request:
branches: [ main ]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Run type tests
run: npm run test:types |
|
Расширенные методы работы с типами в корпоративных проектах
В корпоративной среде, где кодовые базы могут достигать миллионов строк кода, эффективная работа с типами требует дополнительных инструментов и методик.
Автоматизация работы с типами
Для улучшения разработки можно использовать инструменты автоматизации:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Автоматическая генерация типов из моделей базы данных
import { Model, DataTypes } from 'sequelize';
import { generateTypes } from 'sequelize-typescript-generator';
class UserModel extends Model {
static init(sequelize) {
super.init({
id: {
type: DataTypes.UUID,
primaryKey: true
},
name: DataTypes.STRING,
email: {
type: DataTypes.STRING,
unique: true
}
}, { sequelize });
}
}
// Генерация типов из модели
generateTypes([UserModel], 'src/types/generated'); |
|
Типизация паттернов проектирования
Популярные паттерны проектирования также могут быть типизированы для повышения надёжности кода:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| // Типизированный паттерн Наблюдатель (Observer)
interface Observer<T> {
update(data: T): void;
}
class Subject<T> {
private observers: Observer<T>[] = [];
attach(observer: Observer<T>): void {
if (!this.observers.includes(observer)) {
this.observers.push(observer);
}
}
detach(observer: Observer<T>): void {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify(data: T): void {
for (const observer of this.observers) {
observer.update(data);
}
}
}
// Использование
interface UserUpdateEvent {
userId: string;
field: 'name' | 'email';
newValue: string;
}
class UserService extends Subject<UserUpdateEvent> {
updateUserField(userId: string, field: 'name' | 'email', value: string): void {
// Логика обновления
// Уведомление наблюдателей
this.notify({ userId, field, newValue: value });
}
}
class NotificationService implements Observer<UserUpdateEvent> {
update(data: UserUpdateEvent): void {
console.log(`User ${data.userId} changed ${data.field} to ${data.newValue}`);
}
}
// Настройка
const userService = new UserService();
const notificationService = new NotificationService();
userService.attach(notificationService);
// Использование
userService.updateUserField('user-123', 'name', 'John Doe'); |
|
Типизация паттернов проектирования делает код не только надёжнее, но и более понятным, так как типы служат дополнительной документацией.
Типизация в реальных проектах выходит далеко за рамки простого объявления интерфейсов. Она включает в себя вопросы организации кода, управления изменениями, взаимодействия с другими системами и обеспечения надёжности в долгосрочной перспективе. Правильно настроенная система типов становится не обузой, а ценным инструментом, помогающим командам разработчиков создавать качественные и поддерживаемые приложения.
Создать редактор радиосхем для MVC5, используя TypeScript Ребята!)
Нужна инфа как возможно создать редактор,используя typescript (js нежелательно),на mvc 5... Разбиение скомилированного Typescript на файлы В проекте имеется множество typescript файлов, которые компилируются в один js. Но часть скриптов... Изучение TypeScript - советы Нуждаюсь в срочном освоении TypeScript. Поделитесь ресурсами, пожалуйста. Можно на русском и... Не понятен пример кода из спецификации TypeScript Читаю про объектные типы в спецификации на странцие 13, но не понятно из описания как устроен и... Передать свойство класса в анонимную функцию TypeScript как передать значение свойства класса в анонимную функцию.
например следующий код работает... TypeScript "PreComputeCompileTypeScript" how to fix in project Выскакивает ошибка
Везде исправление данной ошибки идёт путём редактирования файла ... Решение кольцевых зависимостей TypeScript + RequreJS Всем привет, возникла проблема с кольцевыми зависимостями при использовании наследования в... TypeScript | extends error Собственно, сделал такой класс:
class trueDate extends Date {
constructor(date: string)... Сохранение this в TypeScript Доброго дня. Подскажите пожалуйста, как можно сохранить this класса так, чтобы можно было... C# класс в TypeScript класс (перенос сущностей) делается бекенд на C#, фронтенд на ts, общаются через REST API (http)
сериализация обьектов... не могу настроить react + typescript в webstorm. Есть люди кто это сделал? Помогите, а то что то туплю уже и пробовал библиотеку скаченную подключать и ссылками. но так... Лучшие видео ресурсы о программировании, angualr 2, typeScript, react и все сомое вкусное Всем доброй поры времени.
Я нашел новый для себя, и как не странно, вообще новый ресур, с видео...
|