TypeScript_Core
TypeScript Core
TypeScript — надмножество JavaScript со статической проверкой типов. Типы помогают обнаруживать ошибки до запуска, безопаснее рефакторить код и документировать контракты. После компиляции большинство типовых конструкций удаляется.
function greet(name: string): string {
return `Привет, ${name}!`;
}
greet("Анна");
// greet(42); // ошибка типовTypeScript не проверяет реальные ответы API, JSON и пользовательский ввод во время выполнения. Внешние данные нужно валидировать отдельно.
Установка
npm install -D typescript
npx tsc --init
npx tsc --noEmit{
"scripts": {
"typecheck": "tsc --noEmit"
}
}Расширения: .ts — TypeScript, .tsx — TypeScript с JSX, .d.ts — декларации типов.
Базовые типы и аннотации
Аннотация записывается после имени переменной, параметра или функции:
let username: string = "alex";
let age: number = 28;
let active: boolean = true;
function add(a: number, b: number): number {
return a + b;
}TypeScript часто выводит тип самостоятельно:
const title = "TypeScript"; // string
const count = 10; // number
const enabled = true; // booleanНе стоит явно указывать очевидный тип. Аннотации особенно полезны для параметров, публичных контрактов, пустых коллекций и nullable-значений.
Основные типы
const text: string = "hello";
const price: number = 99.5;
const visible: boolean = true;
const large: bigint = 9_007_199_254_740_993n;
const key: symbol = Symbol("key");
let selected: string | null = null;
let missing: undefined = undefined;Массивы и кортежи
const ids: number[] = [1, 2, 3];
const names: Array<string> = ["Анна", "Иван"];
const point: [number, number] = [55.75, 37.62];
const response: [status: number, message: string] = [200, "OK"];Массив только для чтения:
const roles: readonly string[] = ["admin", "editor"];
// roles.push("user"); // ошибкаОбъектные типы
const user: { id: number; name: string; active: boolean } = {
id: 1,
name: "Мария",
active: true,
};Повторно используемую структуру обычно выносят в interface или type.
any, unknown, void, never
any отключает проверку типов:
let data: any = "text";
data.missingMethod(); // компилятор не остановит ошибкуunknown принимает любое значение, но требует проверки:
function printValue(value: unknown): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
}
}void обычно обозначает отсутствие используемого результата. never — значение, которое не может возникнуть:
function log(message: string): void {
console.log(message);
}
function fail(message: string): never {
throw new Error(message);
}Литеральные типы
type Theme = "light" | "dark" | "system";
let theme: Theme = "dark";
// theme = "blue"; // ошибкаСужение типов
Перед использованием union-типа нужно уточнить конкретный вариант:
function format(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2);
}
return value.trim();
}Для сужения применяются typeof, instanceof, оператор in, сравнение литеральных полей и пользовательские проверки.
function getErrorMessage(error: unknown): string {
return error instanceof Error
? error.message
: "Неизвестная ошибка";
}Type guard:
type User = { id: number; name: string };
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) return false;
const item = value as Record<string, unknown>;
return typeof item.id === "number" && typeof item.name === "string";
}Приведение as User не проверяет данные во время выполнения.
Интерфейсы
Интерфейс описывает форму объекта или контракт класса:
interface User {
readonly id: number;
name: string;
email?: string;
}
const user: User = {
id: 1,
name: "Анна",
};? делает свойство опциональным. readonly запрещает присваивание на этапе проверки типов, но не замораживает объект в JavaScript.
Расширение
interface Person {
name: string;
}
interface Employee extends Person {
department: string;
}Интерфейс может расширять несколько интерфейсов и объединяться с другой декларацией того же имени.
Индексная сигнатура
interface Dictionary {
[key: string]: string;
}
const labels: Dictionary = {
save: "Сохранить",
cancel: "Отмена",
};Псевдонимы типов
type создаёт имя для любого типа:
type UserId = string | number;
type Coordinates = [number, number];
type Status = "idle" | "loading" | "success" | "error";
type Product = {
id: number;
title: string;
price: number;
};interface или type
| Возможность | interface |
type |
|---|---|---|
| Объектный контракт | Да | Да |
| Расширение | extends |
& |
| Union и кортеж | Нет | Да |
| Слияние деклараций | Да | Нет |
| Utility/условные типы | Ограниченно | Да |
interface удобен для расширяемых объектных контрактов и классов. type — для union, intersection, кортежей, функций и преобразований типов. Важнее придерживаться последовательного стиля проекта.
Union и Intersection
Union | означает «один из вариантов»:
type Id = string | number;
function normalizeId(id: Id): string {
return typeof id === "number" ? String(id) : id.toLowerCase();
}Дискриминируемое объединение удобно для состояний:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };function renderState(state: RequestState<string[]>): string {
switch (state.status) {
case "idle": return "Ожидание";
case "loading": return "Загрузка";
case "success": return `Элементов: ${state.data.length}`;
case "error": return state.message;
}
}Intersection & объединяет требования типов:
type Entity = { id: number };
type Timestamped = { createdAt: string };
type Article = Entity & Timestamped & { title: string };Объект Article обязан содержать поля всех частей пересечения.
Enum
enum Direction {
Up,
Right,
Down,
Left,
}
enum UserRole {
Admin = "admin",
Editor = "editor",
Viewer = "viewer",
}Числовые enum по умолчанию начинаются с 0. Строковые значения удобнее при логировании и передаче данных.
Частая альтернатива — объект с as const:
const UserRole = {
Admin: "admin",
Editor: "editor",
Viewer: "viewer",
} as const;
type UserRole = (typeof UserRole)[keyof typeof UserRole];Такой вариант остаётся обычным JavaScript-объектом и позволяет получить union его значений.
Типизация функций
type Operation = (a: number, b: number) => number;
const multiply: Operation = (a, b) => a * b;Опциональные параметры и значения по умолчанию:
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
function createPage(page = 1, pageSize = 20): string {
return `?page=${page}&pageSize=${pageSize}`;
}Rest-параметры:
function sum(...values: number[]): number {
return values.reduce((total, value) => total + value, 0);
}Callback:
function filterNumbers(
values: number[],
predicate: (value: number) => boolean,
): number[] {
return values.filter(predicate);
}Асинхронная функция возвращает Promise:
async function loadUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<User>;
}Generic-параметр и as здесь не валидируют фактический JSON.
Перегрузка
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
return typeof value === "number" ? value.toFixed(2) : value.trim();
}Перегрузки полезны, если результат зависит от формы вызова. В простых случаях достаточно union-типа.
Дженерики
Дженерики сохраняют связь между входным и выходным типом:
function identity<T>(value: T): T {
return value;
}
const text = identity("hello"); // string
const count = identity(42); // numberРабота с массивом:
function first<T>(items: T[]): T | undefined {
return items[0];
}Несколько параметров типа:
function createPair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}Generic-структура:
interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
type Paginated<T> = {
items: T[];
page: number;
total: number;
};Ограничение через extends:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello");
getLength([1, 2, 3]);Типобезопасный доступ к свойству:
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user = { id: 1, name: "Анна", active: true };
const name = getProperty(user, "name"); // stringДженерик нужен, когда связывает несколько типов. Если функция только принимает неизвестное значение, часто достаточно unknown.
Utility Types
Исходный тип:
interface User {
id: number;
name: string;
email: string;
password: string;
active: boolean;
}Partial<T> и Required<T>
type UserPatch = Partial<User>;
type CompleteUser = Required<User>;Partial делает свойства опциональными, Required — обязательными. Преобразования неглубокие.
Readonly<T>
type ImmutableUser = Readonly<User>;Запрещает присваивание свойствам на этапе компиляции.
Pick<T, K> и Omit<T, K>
type UserPreview = Pick<User, "id" | "name" | "active">;
type PublicUser = Omit<User, "password">;Pick оставляет выбранные свойства, Omit исключает их.
Record<K, V>
type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;
const permissions: Permissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};Другие utility types
type Status = "idle" | "loading" | "success" | "error";
type Finished = Exclude<Status, "idle" | "loading">;
type Pending = Extract<Status, "idle" | "loading">;
type Name = NonNullable<string | null | undefined>;Типы функций:
function createUser() {
return { id: 1, name: "Анна" };
}
type CreatedUser = ReturnType<typeof createUser>;
type FactoryArguments = Parameters<typeof createUser>;
type ResolvedUser = Awaited<Promise<CreatedUser>>;Также доступны ConstructorParameters<T> и InstanceType<T>.
typeof, keyof, as const, satisfies
const settings = {
theme: "dark",
pageSize: 20,
};
type Settings = typeof settings;
type SettingsKey = keyof Settings; // "theme" | "pageSize"
type PageSize = Settings["pageSize"]; // numberas const сохраняет литеральные значения:
const routes = {
home: "/",
users: "/users",
} as const;
type Route = (typeof routes)[keyof typeof routes];
// "/" | "/users"satisfies проверяет контракт, сохраняя точный выведенный тип значения:
type RouteName = "home" | "users";
const routes = {
home: "/",
users: "/users",
} satisfies Record<RouteName, string>;Настройка tsconfig.json
Пример для React-приложения с современным сборщиком:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Основные параметры:
| Параметр | Назначение |
|---|---|
target |
Версия выходного JavaScript |
lib |
Типы стандартных API среды |
module |
Формат модулей |
moduleResolution |
Алгоритм поиска модулей |
jsx |
Обработка JSX |
strict |
Набор строгих проверок |
noEmit |
Проверка без генерации JS |
rootDir / outDir |
Исходная и выходная папки |
paths |
Алиасы импортов |
include / exclude |
Файлы проекта |
paths нужно также настроить в сборщике и тестовой среде: один tsconfig.json не меняет импорты во время выполнения.
Для Node.js без сборщика часто используют:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"rootDir": "src",
"outDir": "dist",
"sourceMap": true
},
"include": ["src/**/*.ts"]
}npm install -D @types/nodeТипизация React
Компоненты с JSX хранятся в .tsx.
Props
type UserCardProps = {
name: string;
age?: number;
active: boolean;
};
export function UserCard({ name, age, active }: UserCardProps) {
return (
<article>
<h2>{name}</h2>
{age !== undefined && <p>Возраст: {age}</p>}
<p>{active ? "Активен" : "Неактивен"}</p>
</article>
);
}children
import type { ReactNode } from "react";
type CardProps = {
title: string;
children: ReactNode;
};
function Card({ title, children }: CardProps) {
return <section><h2>{title}</h2>{children}</section>;
}Стандартные HTML-props
import type { ComponentPropsWithoutRef } from "react";
type ButtonProps = ComponentPropsWithoutRef<"button"> & {
variant?: "primary" | "secondary";
};
function Button({ variant = "primary", ...props }: ButtonProps) {
return <button className={`button--${variant}`} {...props} />;
}useState
const [count, setCount] = useState(0);
const [query, setQuery] = useState("");
const [user, setUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);Для пустого массива и null обычно нужен явный generic.
Состояние запроса:
type LoadState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };
const [state, setState] = useState<LoadState<User[]>>({
status: "idle",
});События
import type { ChangeEvent, FormEvent } from "react";
function SearchForm() {
const [query, setQuery] = useState("");
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setQuery(event.currentTarget.value);
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={handleChange} />
</form>
);
}В inline-обработчике тип события обычно выводится автоматически.
useRef
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);DOM-ссылка изначально равна null, потому что элемент ещё не смонтирован.
useReducer
type State = { count: number };
type Action =
| { type: "increment" }
| { type: "set"; value: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "set": return { count: action.value };
}
}Context
import { createContext, useContext, type ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme используется вне ThemeProvider");
}
return context;
}Типизированный кастомный хук
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(initialValue);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}as const сохраняет результат как кортеж. Данные, прочитанные из localStorage, всё равно нужно проверять во время выполнения.
Типизация API
Удобно разделять сетевую и доменную модели:
type UserDto = {
id: number;
full_name: string;
created_at: string;
};
type User = {
id: number;
name: string;
createdAt: Date;
};
function mapUser(dto: UserDto): User {
return {
id: dto.id,
name: dto.full_name,
createdAt: new Date(dto.created_at),
};
}Безопасная граница данных:
async function fetchJson(url: string): Promise<unknown> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async function loadUser(id: number): Promise<User> {
const value = await fetchJson(`/api/users/${id}`);
if (!isUser(value)) throw new Error("Некорректный формат ответа");
return { ...value, createdAt: new Date() };
}В реальном проекте type guard должен проверять все поля контракта; также можно применять библиотеки схемной валидации.
Частые ошибки
Злоупотребление any
any распространяется по коду и скрывает ошибки. Для неизвестных данных предпочтительнее unknown с проверкой.
Необоснованный as
const user = response as User;Такая запись ничего не проверяет. Не используйте двойное приведение as unknown as T, чтобы просто подавить ошибку.
Оператор ! без гарантии
const element = document.querySelector("#app")!;Оператор не создаёт элемент. Безопаснее проверить значение и выбросить понятную ошибку.
Противоречивые состояния
Вместо нескольких опциональных полей loading?, data?, error? используйте дискриминируемый union.
Игнорирование undefined
Обращение к отсутствующему индексу массива возвращает undefined. Опция noUncheckedIndexedAccess помогает учитывать это в типах.
Типы вместо валидации и тестов
TypeScript не гарантирует корректность бизнес-логики и внешних данных. Типы дополняют runtime-валидацию и тесты, а не заменяют их.
Рекомендации
- Включайте
strictв новых проектах. - Используйте вывод типов там, где тип очевиден.
- Явно описывайте публичные контракты и границы модулей.
- Принимайте внешние данные как
unknownи проверяйте их. - Моделируйте состояния через union-типы.
- Используйте дженерики для связи входных и выходных типов.
- Применяйте
Pick,Omit,PartialиRecordдля осмысленных производных типов. - Используйте
readonly, если данные не должны изменяться. - Запускайте
tsc --noEmitв CI вместе с тестами и линтерами.
Краткая памятка
interface User {
readonly id: number;
name: string;
email?: string;
}
type Id = string | number;
type Admin = User & { permissions: string[] };
type UserPatch = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
type EditableUser = Omit<User, "id">;
type UsersById = Record<string, User>;
function first<T>(items: T[]): T | undefined {
return items[0];
}
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}Главный принцип TypeScript: типы должны описывать реальные допустимые состояния программы. Чем точнее модель отражает предметную область и границы данных, тем больше ошибок компилятор обнаружит до запуска приложения.