$ sudo teach IT
МОДУЛЬ 3 · УРОК 3

Явное указание типа

Синтаксис явной аннотации типов, когда TypeScript не может определить тип самостоятельно и когда вам нужно явно указать тип для переменных, функций, массивов и объектов

~25 минут Для новичков

Приветствую!

Добро пожаловать на третий урок третьего модуля! Сегодня мы разберём один из самых важных аспектов работы с TypeScript — явное указание типов. Вы уже знаете, что TypeScript умеет сам определять типы переменных (это называется type inference, или вывод типов). Но бывают ситуации, когда TypeScript не может или не должен угадывать тип за вас. В таких случаях вы должны явно написать типсобственной руками. Это ключевой навык, который вы будете использовать каждый день.

Цели урока

1
Освоить синтаксис явной аннотации
2
Понять разницу между any и unknown
3
Научиться аннотировать функции и массивы
4
Знать, когда inference недостаточен

1. Синтаксис: variable: Type = value

Давайте начнём с самого главного — синтаксиса. В TypeScript чтобы явно указать тип переменной, вы пишете имя переменной, затем двоеточие, затем имя типа, и наконец присваиваете значение. Выглядит это так:

Базовый синтаксис

// Синтаксис: let имяПеменной: тип = значение;

let возраст: number = 25;
let имя: string = "Алексей";
let активен: boolean = true;

console.log(возраст);   // 25
console.log(имя);       // "Алексей"
console.log(активен);   // true

Обратите внимание на структуру: сначала идёт ключевое слово let (или const), затем имя переменной, потом двоеточие :, затем тип, затем знак равенства =, и наконец значение. Это основной паттерн, который вы будете использовать тысячи раз.

Совет преподавателя: Двоеточие — это ключевой символ. Без двоеточия TypeScript подумает, что вы просто присваиваете значение без аннотации типа. Двоеточие говорит TypeScript: «Эй, я сейчас скажу тебе, какого типа будет эта переменная!»

Вы можете аннотировать тип переменной без немедленного присваивания значения. Это называется «объявление с аннотацией типа». Переменная будет существовать, но пока не иметь значения:

Аннотация без присваивания

// Объявляем переменную с типом, но без значения
let баланс: number;
let статус: string;
let подтвержден: boolean;

// Позже присваиваем значения
баланс = 1500.50;
статус = "активный";
подтвержден = true;

console.log(баланс);      // 1500.50
console.log(статус);      // "активный"
console.log(подтвержден); // true

Это полезно, когда вы пока не знаете значение, но уже знаете, какого типа оно будет. Например, значение может прийти от пользователя или из базы данных позже.

Важно: Если вы объявили переменную с типом, но без значения, а затем попытаетесь присвоить значение другого типа, TypeScript выдаст ошибку. Это и есть главная цель явной аннотации — контроль типов!

Пример ошибки типа

let возраст: number = 25;

// Ошибка! Нельзя присвоить строку переменной типа number
возраст = "двадцать пять";
//  ~~~~~
// Error: Type 'string' is not assignable to type 'number'.

// Ошибка! Нельзя присвоить boolean переменной типа number
возраст = true;
//  ~~~~
// Error: Type 'boolean' is not assignable to type 'number'.

// Всё правильно — number присваивается number
возраст = 30;  // OK!

Вот в чём магия явной аннотации: TypeScript теперь контролирует, какие значения могут попасть в эту переменную. Если вы сказали, что переменная — число, то и значения там будут только числовые. Это защищает от ошибок на ранней стадии.

Вы также можете аннотировать тип для переменных, объявленных через const. Разница в том, что константу нельзя переприсвоить:

Аннотация с const

const PI: number = 3.14159;
const НАЗВАНИЕ_КОМПАНИИ: string = "TechCorp";
const МАКСИМАЛЬНЫЙ_ВОЗРАСТ: number = 150;

// const нельзя переприсвоить:
// PI = 3.14; // Error: Assignment to constant variable.

console.log(PI);                  // 3.14159
console.log(НАЗВАНИЕ_КОМПАНИИ);  // "TechCorp"

Полезно знать: Когда вы используете const с немедленным присваиванием, TypeScript обычно сам выводит литеральный тип (например, "abc" вместо string). Явная аннотация расширяет тип до общего типа. Об этом мы поговорим подробнее в разделе о сравнении explicit vs inference.

2. Явные типы для примитивов

TypeScript имеет несколько базовых (примитивных) типов. Давайте рассмотрим каждый из них и покажем, как явно аннотировать переменные каждого типа. Вы уже видели number, string и boolean. Теперь давайте посмотрим на все примитивы.

Все примитивные типы

// 1. number — числа (целые и дробные)
let возраст: number = 28;
let рост: number = 1.75;
let коэффициент: number = 0.99;
let отрицательное: number = -42;
let ноль: number = 0;
let бесконечность: number = Infinity;
let не_число: number = NaN;

// 2. string — строки
let имя: string = "Мария";
let фамилия: string = 'Петрова';
let приветствие: string = `Привет, ${имя}!`;

// 3. boolean — логический тип (true/false)
let активен: boolean = true;
let удалён: boolean = false;
let есть_ошибки: boolean = false;

// 4. null — значение null
let пусто: null = null;

// 5. undefined — значение undefined
let не_определено: undefined = undefined;

// 6. bigint — большие целые числа
let большое_число: bigint = 9007199254740991n;

// 7. symbol — уникальные идентификаторы
let уникальный_ключ: symbol = Symbol("ключ");

console.log(возраст);        // 28
console.log(имя);            // "Мария"
console.log(активен);        // true
console.log(пусто);          // null
console.log(не_определено);  // undefined
console.log(большое_число);  // 9007199254740991n

Как видите, каждый примитивный тип имеет свой синтаксис аннотации. Давайте подробнее разберём каждый.

Тип number

Тип number покрывает все числовые значения: целые, дробные, отрицательные, положительные,.special-значения как NaN и Infinity. В JavaScript все числа хранятся как 64-битные числа с плавающей запятой, и TypeScript это наследует.

// Примеры чисел разных видов
let целое: number = 42;
let дробное: number = 3.14;
let отрицательное: number = -100;
let ноль: number = 0;
let научная_запись: number = 1.5e10;   // 15000000000
let шестнадцатеричное: number = 0xFF;  // 255
let восьмеричное: number = 0o77;       // 63
let двоичное: number = 0b1010;         // 10
let inf: number = Infinity;
let notANumber: number = NaN;

// Все это — допустимые значения для number:
let тест: number = 42;          // OK
тест = 3.14;                    // OK
тест = -0;                      // OK
тест = Number.MAX_SAFE_INTEGER; // OK (9007199254740991)
тест = Number.NaN;              // OK

Тип string

Тип string предназначен для строк текста. Строки в JavaScript (и TypeScript) могут быть записаны в трёх формах: двойные кавычки, одинарные кавычки и шаблонные строки (backticks). Все три формата создают значения типа string.

// Три способа записать строку
let строка1: string = "Двойные кавычки";
let строка2: string = 'Одинарные кавычки';
let строка3: string = `Шаблонная строка`;

// Шаблонные строки с интерполяцией
let имя: string = "Иван";
let возраст: number = 25;
let приветствие: string = `Меня зовут ${имя}, мне ${возраст} лет`;

console.log(приветствие);
// "Меня зовут Иван, мне 25 лет"

// Многострочная строка
let многострочная: string = `Это
многострочная
строка`;

// Специальные символы
let с_переносом: string = "Строка\nс переносом";
let с_табуляцией: string = "Строка\tс табуляцией";
let с_кавычками: string = "Она сказала \"Привет!\"";

// Пустая строка тоже string
let пустая: string = "";

Тип boolean

Тип boolean может принимать только два значения: true или false. Это логический тип, который широко используется в условиях и флагах.

let включён: boolean = true;
let выключен: boolean = false;
let есть_доступ: boolean = true;
let подтвреждён: boolean = false;

// Результаты сравнений тоже boolean
let x: number = 10;
let больше: boolean = x > 5;    // true
let равно: boolean = x === 10;  // true
let не_равно: boolean = x !== 5; // true

// Логические операции
let a: boolean = true;
let b: boolean = false;
let и: boolean = a && b;      // false (И)
let или: boolean = a || b;     // true (ИЛИ)
let не: boolean = !a;           // false (НЕ)

console.log(больше);   // true
console.log(и);       // false
console.log(или);     // true
console.log(не);      // false

Типы null и undefined

null и undefined — это два отдельных типа, которые оба представляют «пустоту», но по-разному:

// null — «осознанное» отсутствие значения
let результат: null = null;

// undefined — «неопределённое» значение (значение по умолчанию)
let переменная: undefined = undefined;

// Разница в поведении:
let a: string | null = null;    // может быть строкой или null
let b: string | undefined = undefined; // может быть строкой или undefined

// null — это когда мы ЯВНО говорим «значения нет»
let пользователь: string | null = null;
пользователь = "Анна"; // Теперь значение есть

// undefined — это когда значение «ещё не пришло» или «не было передано"]
let настройки: string | undefined;
console.log(настройки); // undefined (значение не было присвоено)

Совет преподавателя: На практике вы будете чаще использовать null для обозначения «значения нет» и undefined для «значение не было передано». Однако в TypeScript по умолчанию null и undefined присваиваются любому типу. Чтобы это ограничить, включите строгий режим через strictNullChecks в tsconfig.json.

Типы bigint и symbol

bigint используется для работы с очень большими целыми числами, которые не вмещаются в обычный number. symbol создаёт уникальные неизменяемые идентификаторы.

// bigint — для очень больших чисел
let большое: bigint = 9007199254740991n;  // Обратите внимание на 'n' в конце
let другое: bigint = BigInt(9007199254740991);

// Обычный number теряет точность для таких значений:
let обычное: number = 9007199254740992;
console.log(обычное);      // 9007199254740992
console.log(обычное + 1);  // 9007199254740992 (теряется точность!)

// bigint сохраняет точность:
console.log(большое);      // 9007199254740991n
console.log(большое + 1n); // 9007199254740992n (точно!)

// symbol — уникальные идентификаторы
let id1: symbol = Symbol("id");
let id2: symbol = Symbol("id");

console.log(id1 === id2); // false (каждый Symbol уникален)

// Символы полезны как ключи объектов
let ключ: symbol = Symbol("секрет");
let объект = {
  [ключ]: "секретное значение"
};
console.log(объект[ключ]); // "секретное значение"

На практике: В 99% случаев вы будете работать с number, string и boolean. Остальные типы встречаются реже, но важно о них знать.

3. any и unknown — зачем и когда

TypeScript предоставляет два «специальных» типа: any и unknown. Оба позволяют хранить значение любого типа, но работают они принципиально по-разному. Разобраться в этой разнице — ключевое для понимания безопасности типов.

Тип any — «выключатель» проверки типов

Тип any по сути отключает проверку типов для переменной. Вы можете присвоить ей что угодно и вызвать любые методы — TypeScript не будет ругаться. Звучит удобно, но на деле это опасно, потому что вы теряете все преимущества TypeScript.

Примеры с any

// any принимает ЛЮБОЕ значение
let что_угодно: any = 42;
что_угодно = "теперь строка";
что_угодно = true;
что_угодно = null;
что_угодно = [1, 2, 3];
что_угодно = { ключ: "значение" };

// any позволяет вызывать ЛЮБЫЕ методы — даже несуществующие!
let переменная: any = "Привет";
console.log(переменная.toUpperCase());  // OK (строковый метод)
console.log(переменная.несуществующийМетод());  // OK! (ошибка только в рантайме)
console.log(переменная * 2);  // OK! (хотя это бессмысленно)

// any отключает проверку аргументов функции
function сложить(a: number, b: number): number {
  return a + b;
}

let значение: any = "Привет";
сложить(значение, 42);  // OK! (хотя "Привет" — не число)
// Ошибка произойдёт ТОЛЬКО при выполнении

Внимание! Использование any — это как надеть «повязку на глаза» TypeScript. Вы перестаёте получать пользу от статической проверки типов. По возможности избегайте any и используйте unknown вместо него.

Тип unknown — безопасная альтернатива any

Тип unknown тоже принимает значение любого типа, но в отличие от any, он требует, чтобы вы проверили тип перед использованием значения. Это «безопасный» способ работать с неизвестными данными.

Примеры с unknown

// unknown принимает любое значение
let что_угодно: unknown = 42;
что_угодно = "строка";
что_угодно = true;
что_угодно = [1, 2, 3];

// НО! Нельзя использовать unknown без проверки типа:
// что_угодно.toUpperCase();  // Error: Object is of type 'unknown'
// что_угодно * 2;           // Error: Operator '*' cannot be applied to types 'unknown' and 'number'

// Правильный способ — проверить тип:
let значение: unknown = "Привет";

if (typeof значение === "string") {
  console.log(значение.toUpperCase());  // OK! TypeScript теперь знает, что это string
}

// Или с помощью switch:
let данные: unknown = 42;

switch (typeof данные) {
  case "string":
    console.log(данные.toUpperCase());  // OK в этой ветке
    break;
  case "number":
    console.log(данные.toFixed(2));     // OK в этой ветке
    break;
  default:
    console.log("Неизвестный тип");
}

// Или с помощью Assertion (если вы УВЕРены в типе):
let вход: unknown = "Привет";
let строка: string = вход as string;  // Утверждаем, что это string
console.log(строка.toUpperCase());

Главное отличие: any пропускает всё, а unknown заставляет вас проверить тип перед использованием. Это как разница между дверью без замка (any) и дверью с замком, для которого нужно знать код (unknown).

Сравнение any и unknown

// === ANY ===
let а: any = "Привет";
// Всё разрешено:
а.toUpperCase();  // OK
а * 2;            // OK
а.foo();          // OK (ошибка только в рантайме!)
а = 42;
а.toFixed();      // OK
а = true;
а.toUpperCase();  // OK (хотя это boolean!)

// === UNKNOWN ===
let б: unknown = "Привет";
// Ничего не разрешено без проверки:
// б.toUpperCase();  // Error!
// б * 2;            // Error!

// Только после проверки:
if (typeof б === "string") {
  б.toUpperCase();  // OK!
}

// unknown — это как сказать:
// "Я пока не знаю, что тут лежит,
//  но я РАЗБЕРУСЬ, прежде чем использовать"

Правило: Если вам нужно переменной хранить данные неизвестного типа, используйте unknown, а не any. any оправдан только при миграции с JavaScript или при работе с библиотеками без типов.

Когда вы всё же используете any, TypeScript позволяет присваивать any-переменной любые значения и передавать её куда угодно:

// any является both "верхним" и "нижним" типом
let значение: any = "Привет";

// Можно присвоить any в переменную любого типа:
let число: number = значение;     // OK!
let строка: string = значение;    // OK!
let булево: boolean = значение;   // OK!

// Можно передать any в функцию с любым параметром:
function принятьЧисло(n: number) { console.log(n * 2); }
function принятьСтроку(s: string) { console.log(s.length); }

принятьЧисло(значение);   // OK! (хотя там строка)
принятьСтроку(значение);  // OK!

// unknown НЕ позволяет этого:
let неизвестное: unknown = "Привет";
// принятьЧисло(неизвестное);   // Error!
// принятьСтроку(неизвестное);  // Error!

4. Явные типы для функций

Аннотация типов для функций включает три части: типы параметров, тип возвращаемого значения и тип самой функции. Давайте разберём каждую.

Типы параметров функции

Каждый параметр функции может иметь явную аннотацию типа. Это говор TypeScript: «Я ожидаю, что сюда передадут значение этого типа».

Аннотация параметров

// Функция с аннотированными параметрами
function поздороваться(имя: string, возраст: number): string {
  return `Привет, ${имя}! Тебе ${возраст} лет.`;
}

// Правильные вызовы:
console.log(поздороваться("Анна", 25));
// "Привет, Анна! Тебе 25 лет."

// Ошибка! Неверный тип аргумента:
// поздороваться(25, "Анна");  // Error: argument of type 'number' is not assignable

// Функция с несколькими параметрами разных типов
function создатьЧеловека(
  имя: string,
  фамилия: string,
  возраст: number,
  активен: boolean
): string {
  return `${имя} ${фамилия}, ${возраст} лет, активен: ${активен}`;
}

let описание = создатьЧеловека("Иван", "Петров", 30, true);
console.log(описание);
// "Иван Петров, 30 лет, активен: true"

TypeScript обычно выводит типы параметров из контекста вызова, но явная аннотация делает код самодокументируемым и защищает от ошибок.

Тип возвращаемого значения

После закрывающей скобки параметров вы можете указать тип, который функция возвращает. Это делается через двоеточие и имя типа:

Аннотация возвращаемого типа

// Функция, возвращающая number
function сложить(a: number, b: number): number {
  return a + b;
}

// Функция, возвращающая string
function получитьПриветствие(имя: string): string {
  return `Привет, ${имя}!`;
}

// Функция, возвращающая boolean
function являетсяЧётным(число: number): boolean {
  return число % 2 === 0;
}

// Функция, ничего не возвращающая (void)
function вывестиСообщение(text: string): void {
  console.log(text);
}

// Функция, никогда не возвращающая (never)
function выброситьОшибку(сообщение: string): never {
  throw new Error(сообщение);
}

// Проверяем:
console.log(сложить(5, 3));               // 8
console.log(получитьПриветствие("Мир"));  // "Привет, Мир!"
console.log(являетсяЧётным(4));           // true
console.log(являетсяЧётным(7));           // false
вывестиСообщение("Привет!");              // "Привет!"

Совет преподавателя: TypeScript может вывести возвращаемый тип автоматически. Но явная аннотация возвращаемого типа — это хорошая практика, потому что если вы случайно вернёте не тот тип, TypeScript сразу покажет ошибку.

Рассмотрим пример, когда явная аннотация возвращаемого типа ловит ошибку:

// С явной аннотацией — ошибка видна сразу
function разделить(a: number, b: number): number {
  if (b === 0) {
    return "Деление на ноль!";  // Error: Type 'string' is not assignable to type 'number'
  }
  return a / b;
}

// Без аннотации — TypeScript выведит тип как `number | string`
// и это может привести к ошибкам в дальнейшем
function разделить_2(a: number, b: number) {
  if (b === 0) {
    return "Деление на ноль!";  // OK, TypeScript выведет `string | number`
  }
  return a / b;
}

// Теперь пользователь этой функции может получить неожиданный тип:
let результат = разделить_2(10, 0);
// результат имеет тип `string | number`
// Если попытаться использовать как number:
// результат.toFixed(2);  // Error: Property 'toFixed' does not exist on type 'string | number'

Параметры по умолчанию и необязательные параметры

В TypeScript можно задавать параметрам значения по умолчанию и делать их необязательными. Аннотация типа остаётся обязательной даже в этих случаях:

// Параметр по умолчанию
function создатьПрофиль(
  имя: string,
  возраст: number = 25,           // значение по умолчанию
  активен: boolean = true          // значение по умолчанию
): string {
  return `${имя}, ${возраст} лет, активен: ${активен}`;
}

console.log(создатьПрофиль("Анна"));          // "Анна, 25 лет, активен: true"
console.log(создатьПрофиль("Борис", 30));     // "Борис, 30 лет, активен: true"
console.log(создатьПрофиль("Вера", 28, false)); // "Вера, 28 лет, активен: false"

// Необязательный параметр (с ?)
function отправитьСообщение(
  от: string,
  кому: string,
  текст: string,
  тема?: string               // необязательный параметр
): string {
  let результат = `От ${от} для ${кому}: ${текст}`;
  if (тема) {
    результат = `Тема: ${тема}\n${результат}`;
  }
  return результат;
}

console.log(отправитьСообщение("Анна", "Борис", "Привет!"));
// "От Анна для Борис: Привет!"

console.log(отправитьСообщение("Анна", "Борис", "Привет!", "Вопрос"));
// "Тема: Вопрос
//  От Анна для Борис: Привет!"

Тип функции (Function type)

Иногда вам нужно указать тип для самой переменной-функции. Это полезно, когда функции передаются как аргументы другим функциям:

Аннотация типа функции

// Тип функции: (параметры) => возвращаемыйТип
let операция: (a: number, b: number) => number;

// Присваиваем конкретную функцию
операция = (a, b) => a + b;
console.log(операция(5, 3));  // 8

operация = (a, b) => a * b;
console.log(operация(5, 3));  // 15

// Ошибка! Неправильный тип:
// операция = (a, b) => a + b + "строка";  // Error!

// Пример с callback-функцией
function выполнитьОперацию(
  a: number,
  b: number,
  операция: (x: number, y: number) => number  // тип callback-функции
): number {
  return операция(a, b);
}

let сумма = выполнитьОперацию(10, 5, (a, b) => a + b);
let разность = выполнитьОперацию(10, 5, (a, b) => a - b);
let произведение = выполнитьОперацию(10, 5, (a, b) => a * b);

console.log(сумма);        // 15
console.log(разность);     // 5
console.log(произведение); // 50

Запомните: Стрелка => в типе функции — это не оператор стрелочной функции. Это синтаксис для описания типа: слева параметры, справа — возвращаемый тип.

5. Явные типы для массивов

Массивы в TypeScript аннотируются двумя способами: с помощью квадратных скобок (Type[]) и с помощью обобщённого типа (Array<Type>). Оба способа делают одно и то же.

Два способа аннотации массивов

// Способ 1: Type[]
let числа: number[] = [1, 2, 3, 4, 5];
let строки: string[] = ["Привет", "Мир", "TypeScript"];
let булевы: boolean[] = [true, false, true];

// Способ 2: Array<Type>
let числа2: Array<number> = [1, 2, 3, 4, 5];
let строки2: Array<string> = ["Привет", "Мир", "TypeScript"];
let булевы2: Array<boolean> = [true, false, true];

// Оба способа эквивалентны
// числа === числа2 по типу

// Пустой массив — нужно указать тип!
let пустойМассив: number[] = [];
let пустойМассив2: Array<string> = [];

// Ошибка! Нельзя добавить строку в массив чисел:
// числа.push("шестёрка");  // Error: Argument of type 'string' is not assignable

// Правильно:
числа.push(6);       // OK
console.log(числа);  // [1, 2, 3, 4, 5, 6]

Когда вы создаёте пустой массив и присваиваете ему значение, TypeScript может вывести тип из первого добавленного элемента. Но лучше явно указать тип:

// Массив с несколькими типами (union type в массиве)
let смешанный: (string | number)[] = [1, "два", 3, "четыре"];
let anyМассив: any[] = [1, "два", true, null];

// Массив объектов
interface Человек {
  имя: string;
  возраст: number;
}

let люди: Человек[] = [
  { имя: "Анна", возраст: 25 },
  { имя: "Борис", возраст: 30 },
];

// Без интерфейса — inline тип
let товары: { название: string; цена: number }[] = [
  { название: "Ноутбук", цена: 50000 },
  { название: "Мышь", цена: 1500 },
];

// Массив функций
let функции: ((x: number) => number)[] = [
  (x) => x * 2,
  (x) => x + 10,
  (x) => x ** 2,
];

// Вложенные массивы (массив массивов)
let матрица: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

// Read-only массивы
letCONSTANTНЫЕ: readonly number[] = [1, 2, 3];
// CONSTANTНЫЕ.push(4);  // Error: Property 'push' does not exist on type 'readonly number[]'

Важно: Когда вы создаёте пустой массив с let, TypeScript выводит тип как any[], если не указать тип явно. Это может привести к ошибкам:

// ОПАСНО: TypeScript выведет any[]
let numbers = [];
numbers.push(1);
numbers.push(2);
numbers.push("три");  // OK! (хотя это строка, а не число)

// БЕЗОПАСНО: Явная аннотация
let numbers2: number[] = [];
numbers2.push(1);
numbers2.push(2);
// numbers2.push("три");  // Error: Argument of type 'string' is not assignable

// Также можно использовать Generic с пустым массивом
let numbers3: Array<number> = [];
numbers3.push(1);
// numbers3.push("два");  // Error!

Тип readonly создаёт неизменяемый массив. Это полезно для константных данных:

// Readonly массивы
let дниНедели: readonly string[] = [
  "Понедельник",
  "Вторник",
  "Среда",
  "Четверг",
  "Пятница",
  "Суббота",
  "Воскресенье"
];

// дниНедели.push("Понедельник");     // Error!
// дниНедели[0] = "Другой день";      // Error!
console.log(дниНедели[0]);  // "Понедельник" (только чтение OK)

// Readonly tuple (неизменяемый кортеж)
let координаты: readonly [number, number] = [55.7558, 37.6173];
// координаты.push(0);  // Error!
// координаты[0] = 0;   // Error!

// Readonly Map и Set
let карта: ReadonlyMap<string, number> = new Map([
  ["яблоко", 5],
  ["банан", 3],
]);
// карта.set("груша", 2);  // Error!

6. Явные типы для объектов

Аннотация типов для объектов — одна из самых частых задач. В TypeScript есть несколько способов описать тип объекта: inline типы и интерфейсы/типы (об этом мы поговорим позже). Пока сосредоточимся на inline-описании.

Inline аннотация объекта

// Inline тип объекта
let человек: { имя: string; возраст: number } = {
  имя: "Анна",
  возраст: 25,
};

// Ошибка! Лишнее свойство:
// let человек2: { имя: string; возраст: number } = {
//   имя: "Борис",
//   возраст: 30,
//   email: "boris@mail.com"  // Error: Object literal may only specify known properties
// };

// Ошибка! Не хватает свойства:
// let человек3: { имя: string; возраст: number } = {
//   имя: "Вера",
// };

// Все свойства обязательны по умолчанию!
// Чтобы сделать свойство необязательным, добавьте ?
let настройки: {
  тема: string;
  язык: string;
  уведомления?: boolean;      // необязательное свойство
  размер_шрифта?: number;    // необязательное свойство
} = {
  тема: "тёмная",
  язык: "ru",
};

console.log(настройки.тема);             // "тёмная"
console.log(настройки.уведомления);      // undefined (необязательное)

// Readonly свойства
let конфиг: {
  readonly apiКлюч: string;
  readonly сервер: string;
  порт: number;
} = {
  apiКлюч: "abc123",
  сервер: "https://api.example.com",
  порт: 3000,
};

// конфиг.apiКлюч = "новый";  // Error: Cannot assign to 'apiКлюч' because it is a read-only property
конфиг.порт = 8080;  // OK!

Вы можете аннотировать тип объекта, который содержит методы (функции как свойства):

// Объект с методами
let калькулятор: {
  имя: string;
  сложить: (a: number, b: number) => number;
  вычесть: (a: number, b: number) => number;
  умножить: (a: number, b: number) => number;
} = {
  имя: "Мой калькулятор",
  сложить: (a, b) => a + b,
  вычесть: (a, b) => a - b,
  умножить: (a, b) => a * b,
};

console.log(калькулятор.сложить(5, 3));    // 8
console.log(калькулятор.вычесть(10, 4));   // 6
console.log(калькулятор.умножить(6, 7));   // 42

// Объект с вложенными объектами
let компания: {
  название: string;
  адрес: {
    город: string;
    улица: string;
    дом: number;
  };
  сотрудники: number;
} = {
  название: "TechCorp",
  адрес: {
    город: "Москва",
    улица: "Тверская",
    дом: 15,
  },
  сотрудники: 250,
};

console.log(компания.адрес.город);  // "Москва"

Иногда объекты могут иметь произвольные ключи с известным типом значений. Для этого используется индексная сигнатура:

Индексная сигнатура

// Объект с произвольными строковыми ключами
let словарь: { [ключ: string]: number } = {
  яблоко: 5,
  банан: 3,
  апельсин: 8,
};

console.log(словарь["яблоко"]);  // 5
словарь["груша"] = 2;            // OK!

// Объект с произвольными ключами типа number
let индекс: { [индекс: number]: string } = {
  0: "первый",
  1: "второй",
  2: "третий",
};

// Смешанный тип: известные + произвольные ключи
let данные: {
  id: number;
  имя: string;
  [дополнительное: string]: string | number;
} = {
  id: 1,
  имя: "Товар",
  категория: "Электроника",
  вес: 2.5,
};

console.log(данные.id);          // 1
console.log(данные.имя);         // "Товар"
console.log(данные["категория"]); // "Электроника"
console.log(данные["вес"]);       // 2.5

Полезно знать: Индексные сигнатуры полезны для создания словарей и маппингов. Однако когда ключи известны заранее, лучше использовать интерфейс или тип с конкретными свойствами — это безопаснее и нагляднее.

7. Когда type inference недостаточен

Type inference (вывод типов) — это когда TypeScript сам определяет тип на основе значения. Это удобно, но бывают ситуации, когда inference не справляется или выводит слишком узкий/широкий тип. Вот основные случаи.

Случай 1: Пустой массив

Когда вы создаёте пустой массив, TypeScript не может определить, какие элементы туда будут добавлены:

// TypeScript выведет any[] — это опасно!
let числа = [];
числа.push(1);
числа.push(2);
числа.push("три");  // OK! (но это ошибка логики)

// Правильно — явная аннотация:
let числа2: number[] = [];
числа2.push(1);
числа2.push(2);
// числа2.push("три");  // Error!

// Или через Generic:
let числа3: Array<number> = [];
числа3.push(1);
// числа3.push("два");  // Error!

Случай 2: Переменная без присваивания

Если вы объявляете переменную без значения, TypeScript не может вывести тип:

// Ошибка! Не удалось вывести тип
// let имя;
// Error: Variable 'имя' implicitly has an 'any' type because type 'noImplicitAny' has no good expression.

// Правильно — явная аннотация:
let имя: string;
имя = "Анна";  // OK
// имя = 42;   // Error: Type 'number' is not assignable to type 'string'

// То же для переменных, которые будут заполнены позже
let данные: { id: number; имя: string };
// Данные будут загружены из API позже

Случай 3: Слишком узкий вывод

Иногда TypeScript выводит слишком конкретный (литеральный) тип, а вам нужен более широкий:

// TypeScript выведет литеральный тип "включён"
let статус = "включён";
// статус имеет тип: "включён" (литерал, а не string!)

// Если попытаемся присвоить другое значение:
// статус = "выключен";  // Error: Type '"выключен"' is not assignable to type '"включён"'

// Решение — явная аннотация:
let статус2: string = "включён";
статус2 = "выключен";  // OK! Теперь любой string

// То же с числами:
let ответ = 42;
// ответ имеет тип: 42 (литерал!)

let ответ2: number = 42;
ответ2 = 100;  // OK!

Случай 4: Слишком широкий вывод с const

Когда вы используете const, TypeScript выводит литеральный тип. Это хорошо для значений, но иногда мешает:

// const выводит литеральный тип
const PI = 3.14159;  // тип: 3.14159
const НАЗВАНИЕ = "Мой сайт";  // тип: "Мой сайт"

// Если нужен более широкий тип:
const PI2: number = 3.14159;      // тип: number
const НАЗВАНИЕ2: string = "Мой сайт";  // тип: string

// Это важно для массивов объектов:
const товары = [
  { id: 1, цена: 100 },
  { id: 2, цена: 200 },
];
// тип: readonly { id: number; цена: number; }[]
// (литеральный readonly)

// Если нужно изменяемое:
const товары2: { id: number; цена: number }[] = [
  { id: 1, цена: 100 },
  { id: 2, цена: 200 },
];

Случай 5: Callback-функции

Когда функция принимает callback, TypeScript выводит типы параметров callback из контекста. Но иногда контекста недостаточно:

// TypeScript выведет типы параметров из контекста
[1, 2, 3].forEach((элемент) => {
  console.log(элемент);  // элемент: number (выведено из массива)
});

// Но если нет контекста — нужна аннотация:
let callback: (значение: number) => void;

callback = (значение) => {
  console.log(значение);  // значение: number (из аннотации)
};

// Или в функции:
function выполнить(
  данные: number[],
  обработчик: (элемент: number, индекс: number) => void
) {
  данные.forEach((элемент, индекс) => {
    обработчик(элемент, индекс);
  });
}

выполнить([10, 20, 30], (элемент, индекс) => {
  console.log(`${индекс}: ${элемент}`);
});

Случай 6: Деструктуризация

При деструктуризации объектов TypeScript выводит типы свойств. Но иногда нужно уточнить:

// Деструктуризация с выводом типов
let человек = { имя: "Анна", возраст: 25, город: "Москва" };
let { имя, возраст } = человек;
// имя: string, возраст: number (выведено)

// Деструктуризация с аннотацией (редко нужно)
function обработать({
  x,
  y,
  z = 0,
}: {
  x: number;
  y: number;
  z?: number;
}) {
  console.log(x + y + z);
}

обработать({ x: 1, y: 2 });      // 3
обработать({ x: 1, y: 2, z: 5 }); // 8

Вывод: Type inference — мощный инструмент, который экономит ваше время. Но он не идеален: в пустых коллекциях, без присваивания, при литеральных значениях и в сложных ситуациях — нужна явная аннотация.

8. Когда explicit types required

Существуют ситуации, когда TypeScript требует явного указания типа — вывод типов невозможен или приводит к ошибкам. Давайте рассмотрим основные из них.

1. Объявление переменной без значения

Когда вы объявляете переменную через let или const без присваивания, TypeScript не может вывести тип:

// Ошибка! TypeScript не может вывести тип
// let имя;
// Error: Variable 'имя' implicitly has an 'any' type.

// Правильно:
let имя: string;
имя = "Анна";

// То же с const:
// const КОНСТАНТА;
// Ошибка!

2. Параметры стрелочных функций без контекста

Когда стрелочная функция не имеет контекста вызова, TypeScript не может вывести типы параметров:

// С контекстом — TypeScript выводит типы
[1, 2, 3].map((x) => x * 2);  // x: number

// Без контекста — нужна аннотация:
let умножить = (x) => x * 2;
// Error: Parameter 'x' implicitly has an 'any' type.

// Правильно:
let умножить2 = (x: number) => x * 2;

// Или через переменную с типом функции:
let умножить3: (x: number) => number = (x) => x * 2;

3. Функции, возвращающие разные типы (перегрузка)

Когда функция может возвращать значения разных типов в зависимости от аргументов:

// Функция, которая возвращает разные типы
function найти(массив: string[], ключ: string): string | undefined {
  return массив.find((элемент) => элемент === ключ);
}

let результат = найти(["а", "б", "в"], "б");
// результат: string | undefined

// Если результат undefined — нужна проверка:
if (результат) {
  console.log(результат.toUpperCase());  // OK
} else {
  console.log("Не найдено");
}

// Union type в параметрах
function.format(значение: string | number): string {
  if (typeof значение === "number") {
    return значение.toFixed(2);
  }
  return значение.toUpperCase();
}

console.log(format(3.14159));  // "3.14"
console.log(format("привет")); // "ПРИВЕТ"

4. Работа с DOM

При работе с DOM-элементами TypeScript может вывести тип как HTMLElement | null, и вам нужно явно указать более конкретный тип:

// DOM-элементы — типы HTMLElement | null
let элемент = document.getElementById("myInput");
// тип: HTMLElement | null

// Нужно проверить на null:
if (элемент) {
  // элемент: HTMLElement (уже не null)
  элемент.innerHTML = "Привет!";
}

// Явная аннотация для конкретного типа:
let инпут = document.getElementById("myInput") as HTMLInputElement;
// Теперь инпут: HTMLInputElement
// инпут.value — строка с значением input
// инпут.placeholder — placeholder

// Или с проверкой:
let элемент2 = document.getElementById("myInput");
if (элемент2 instanceof HTMLInputElement) {
  console.log(элемент2.value);
}

5. Массивы с начальными значениями

Когда вы инициализируете массив одним значением, TypeScript выводит тип только этого значения:

// TypeScript выведет number[]
let числа = [1, 2, 3];
// числа: number[]

// Если добавить строку:
// числа.push("четыре");  // Error!

// Но если массив содержит разные типы:
let смешанный = [1, "два", true];
// смешанный: (string | number | boolean)[]

// Явная аннотация даёт контроль:
let только_числа: number[] = [1, 2, 3];
let только_строки: string[] = ["а", "б", "в"];

// Если нужен union type массива:
let any_ тип: (string | number)[] = [1, "два", 3];

6. Callback-функции без контекста

Когда вы передаёте функцию как аргумент, и TypeScript не может вывести её тип из контекста:

// Функция с callback — типы выводятся из контекста
function обработатьДанные(
  данные: number[],
  callback: (значение: number, индекс: number) => void
) {
  данные.forEach((значение, индекс) => {
    callback(значение, индекс);
  });
}

// TypeScript выведет типы callback:
обработатьДанные([1, 2, 3], (значение, индекс) => {
  console.log(`${индекс}: ${значение}`);
});

// Но если функция передаётся не как аргумент:
let мояФункция = (x) => x * 2;
// Error: Parameter 'x' implicitly has an 'any' type.

// Правильно:
let мояФункция2 = (x: number) => x * 2;

// Или через тип функции:
type Обработчик = (значение: number) => string;
let обработчик: Обработчик = (значение) => `Значение: ${значение}`;

Правило: Если TypeScript выдаёт ошибку «implicitly has an 'any' type» — вам нужно явно указать тип. Это защита от случайных ошибок.

9. Частые ошибки новичков

Новички часто допускают одни и те же ошибки при работе с явной аннотацией типов. Давайте разберём самые распространённые из них.

Ошибка 1: Путаница между типом и значением

// ОШИБКА! Путаница типа и значения
let имя: string = string;
//              ~~~~~~
// Error: 'string' refers to a value, but is being used as a name here.

// ПРАВИЛЬНО:
let имя2: string = "Анна";
//  ^^^^     ^^^^^^  ^^^^^^
//  имя      тип     значение

// Ещё пример ошибки:
let возраст: number = number;
//                  ~~~~~~
// Error: 'number' refers to a value, but is being used as a name here.

// Правильно:
let возраст2: number = 25;

Ошибка 2: Забыть двоеточие

// ОШИБКА! Забыли двоеточие
let имя string = "Анна";
//     ~~~~~~
// Error: ',' expected.

// ПРАВИЛЬНО:
let имя2: string = "Анна";
//       ^  этом место для двоеточия

// Ещё пример:
let возраст number = 25;
//        ~~~~~~
// Error: ',' expected.

// Правильно:
let возраст2: number = 25;

Ошибка 3: Лишние скобки в типе массива

// ОШИБКА! Лишние скобки
let числа: (number)[] = [1, 2, 3];
//           ~~~~~~~
// Error: ',' expected. (хотя это часто работает, это плохой стиль)

// ПРАВИЛЬНО (два варианта):
let числа2: number[] = [1, 2, 3];
let числа3: Array<number> = [1, 2, 3];

// ОШИБКА! Скобки вокруг union type
let смешанный: (string | number)[] = [1, "два"];
//           ~~~~~~~~~~~~~~~~~~~
// Это РАБОТАЕТ, но скобки не нужны:
let смешанный2: string | number[] = [1, "два"];
//  Это НЕ то же самое! Это string | (number[])

// ПРАВИЛЬНО:
let смешанный3: (string | number)[] = [1, "два"];
// Или:
let смешанный4: Array<string | number> = [1, "два"];

Ошибка 4: Неправильный порядок в функции

// ОШИБКА! Тип возвращаемого значения не после скобок
function сложить(a: number, b: number) number {
//                                        ~~~~~~
// Error: ',' expected.

// ПРАВИЛЬНО:
function сложить2(a: number, b: number): number {
//                                        ^
// Двоеточие после скобок!

// ОШИБКА! Тип параметра после имени
function сложить3(a number, b number): number {
//           ~~~~~  ~~~~~~  ~~~~~~
// Error: ',' expected.

// ПРАВИЛЬНО:
function сложить4(a: number, b: number): number {
//          ^    ^    ^    ^
// Двоеточие перед типом!

Ошибка 5: Использование any вместо unknown

// ПЛОХО: Использование any
function processData(data: any): any {
  return data.foo.bar.baz;  // Ошибка только в рантайме!
}

// ХОРОШО: Использование unknown
function processData2(data: unknown): string {
  if (
    typeof data === "object" &&
    data !== null &&
    "foo" in data
  ) {
    let obj = data as { foo: { bar: { baz: string } } };
    return obj.foo.bar.baz;
  }
  throw new Error("Неверные данные");
}

// Ещё пример:
let значение: any = "Привет";
значение.toUpperCase();  // OK
значение.несуществующийМетод();  // OK! (ошибка в рантайме)

let значение2: unknown = "Привет";
// значение2.toUpperCase();  // Error!
if (typeof значение2 === "string") {
  значение2.toUpperCase();  // OK!

Ошибка 6: Забыть про null/undefined

// ОШИБКА! Не учитываем null
function найтиПользователя(id: number): { имя: string } {
  // Может вернуть null, если пользователь не найден
  let пользователь = базаДанных.get(id);
  return пользователь;  // Error: Type 'null' is not assignable
}

// ПРАВИЛЬНО: Учитываем null
function найтиПользователя2(id: number): { имя: string } | null {
  let пользователь = базаДанных.get(id);
  return пользователь;
}

// Или с union type:
function найтиПользователя3(id: number): { имя: string } | undefined {
  return базаДанных.get(id);
}

// Использование:
let пользователь = найтиПользователя3(1);
if (пользователь) {
  console.log(пользователь.имя);
}

Ошибка 7: Избыточная аннотация

// Избыточно: тип и так очевиден
let возраст: number = 25;           // number очевиден
let имя: string = "Анна";           // string очевиден
let активен: boolean = true;         // boolean очевиден

// Достаточно:
let возраст2 = 25;                  // TypeScript выведет number
let имя2 = "Анна";                  // TypeScript выведет "Анна" (литерал)
let активен2 = true;                // TypeScript выведет true (литерал)

// Когда аннотация нужна:
let пусто: number[] = [];           // Нужна! TypeScript выведет any[]
let значение: string | null = null; // Нужна! TypeScript выведет null (литерал)

// Принцип: аннотируйте, когда:
// 1. Создаёте пустые коллекции
// 2. Объявляете без присваивания
// 3. Нужен более широкий тип (string вместо "abc")
// 4. Работаете с функциями без контекста

Золотое правило: Аннотируйте когда TypeScript не может вывести тип, когда нужен более широкий тип, и когда вы хотите документировать намерения кода. Не аннотируйте когда тип очевиден из контекста.

10. Сравнение explicit vs inference

Давайте подведём черту и сравним явную аннотацию (explicit) и вывод типов (inference) на конкретных примерах.

Прямое сравнение

// === EXPLICIT (явная аннотация) ===
let a: number = 42;           // тип: number
let b: string = "Привет";     // тип: string
let c: boolean = true;        // тип: boolean
let d: number[] = [1, 2, 3];  // тип: number[]

// === INFERENCE (вывод типов) ===
let e = 42;                   // тип: 42 (литерал!)
let f = "Привет";             // тип: "Привет" (литерал!)
let g = true;                 // тип: true (литерал!)
let h = [1, 2, 3];            // тип: number[] (выведено)

// Разница в литералах:
// a: number — принимает ЛЮБОЕ число
// e: 42 — принимает ТОЛЬКО 42

a = 100;   // OK (number принимает 100)
// e = 100; // Error! 42 не принимает 100

// Чтобы исправить литерал:
let e2: number = 42;  // Теперь тип number
e2 = 100;  // OK!

// То же со строками:
let f2: string = "Привет";
f2 = "Другое";  // OK!

Когда inference лучше

// INFERENCE лучше, когда тип очевиден:

// 1. Простые присваивания
let count = 0;              // number (очевидно)
let name = "TypeScript";    // string (очевидно)
let isActive = true;        // boolean (очевидно)

// 2. Результаты вызовов
let length = "Hello".length;  // number (выведено)
let upper = "hello".toUpperCase();  // string (выведено)

// 3. С callback-функциями (контекстный вывод)
[1, 2, 3].forEach((item) => {
  // item: number (выведено из контекста массива)
  console.log(item);
});

// 4. Сgenerics (Generic) функциями
let result = [1, 2, 3].map((x) => x * 2);
// result: number[] (выведено)

// 5. Деструктуризация
const { x, y } = { x: 1, y: 2 };
// x: number, y: number (выведено)

Когда explicit лучше

// EXPLICIT лучше, когда:

// 1. Пустые коллекции
let numbers: number[] = [];           // инference: any[] (опасно!)
let names: string[] = [];             // инference: any[] (опасно!)

// 2. Без присваивания
let userId: string;                   // инference: ошибка!
let config: Config;                   // инference: ошибка!

// 3. Нужен широкий тип вместо литерала
let status: string = "active";        // инference: "active" (литерал)
let statusCode: number = 200;         // инference: 200 (литерал)

// 4. Функции без контекста
let multiply = (x: number) => x * 2;  // нужна аннотация параметра
let divide: (a: number, b: number) => number =
  (a, b) => a / b;                    // нужна аннотация типа функции

// 5. Работа с external API (unknown)
let response: unknown = fetch(...);   // инference: any (опасно!)
let parsed: User = response as User;  // явное приведение

// 6. Документирование намерений
let MAX_RETRIES: number = 3;         // "это константа"
let DEFAULT_TIMEOUT: number = 5000;  // "это в миллисекундах"

Практические примеры: когда что использовать

// Практический пример: работа с данными пользователя

// EXPLICIT — когда нужна документация и контроль
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Явная аннотация для пустого массива
let users: User[] = [];

// Явная аннотация для функции
function createUser(name: string, email: string, age: number): User {
  return {
    id: Math.random(),
    name,
    email,
    age,
  };
}

// INFERENCE — когда тип очевиден
let newUser = createUser("Анна", "anna@mail.com", 25);
// newUser: User (выведено из return type)

// Явная аннотация для callback
function filterAdults(users: User[]): User[] {
  return users.filter((user) => user.age >= 18);
  // user: User (выведено из контекста)
}

// Явная аннотация для переменной без значения
let selectedUser: User | null = null;

// INFERENCE для результатов
let adults = filterAdults(users);
// adults: User[] (выведено из return type)

Сводная таблица

// СВОДНАЯ ТАБЛИЦА:

// СИТУАЦИЯ                          |  ВЫБОР
// ---------------------------------|------------------
// Простое присваивание              |  inference
// Пустой массив                     |  explicit
// Без присваивания                  |  explicit
// Нужен широкий тип                 |  explicit
// Callback с контекстом             |  inference
// Callback без контекста            |  explicit
// Результат вызова                  |  inference
// Деструктуризация                  |  inference
// Документирование констант         |  explicit
// Работа с external API             |  explicit
// Объектный литерал                 |  inference (обычно)
// Union type                        |  explicit (обычно)
// Generic функции                   |  inference

Золотое правило: По умолчанию доверяйте inference. Используйте explicit только когда: (1) inference не работает (пустые коллекции, без присваивания), (2) нужен другой тип (шире/уже), (3) вы хотите документировать намерения.

Полезные советы

Совет 1: Включите noImplicitAny: true в tsconfig.json. Это заставит TypeScript требовать явную аннотацию там, где он не может вывести тип. Это поможет вам привыкнуть к аннотациям и избежать скрытых any.

Совет 2: Используйте unknown вместо any. any — это лазейка, которая обходит проверку типов. unknown заставляет вас проверять тип перед использованием.

Совет 3: Аннотируйте возвращаемый тип функции, даже если TypeScript может вывести его. Это делает код самодокументируемым и защищает от случайного изменения возвращаемого типа.

Совет 4: Для интерфейсов и типов используйте interface вместо inline-аннотации объектов. Это делает код чище и переиспользуемее.

Совет 5: Не аннотируйте то, что и так очевидно: let x: number = 5 — избыточно. let x = 5 — достаточно.

Итоги урока

Мы подробно изучили явное указание типов в TypeScript. Давайте подведём итоги:

1
Синтаксис: let имя: тип = значение — двоеточие separating имя и тип
2
Примитивы: number, string, boolean, null, undefined, bigint, symbol — каждый имеет свою аннотацию
3
any vs unknown: any отключает проверку, unknown требует проверки. Используйте unknown!
4
Функции: Аннотируйте параметры и возвращаемый тип. Тип функции: (параметры) => возвращаемыйТип
5
Массивы: Type[] или Array<Type>. Пустые массивы требуют явной аннотации!
6
Объекты: Inline-тип: { свойство: тип }. Необязательные свойства через ?
7
Explicit vs Inference: Доверяйте inference, используйте explicit когда нужно: пустые коллекции, широкий тип, документирование

Помните: Явная аннотация типов — это не лишняя работа, а инвестиция в качество кода. Она делает код самодокументируемым, ловит ошибки на ранней стадии и помогает другим разработчикам понять ваш код.

Следующий урок

В следующем уроке мы изучим Массивы и кортежи — как работать с коллекциями данных, аннотировать их типы и использовать все возможности TypeScript для работы с группами значений.

Массивы и кортежи →

Проверяем понимание

8 вопросов

Явная аннотация типов

Premium