Неявное определение типа (type inference)
Как TypeScript сам догадывается, какой тип у переменной — и почему это важно понимать начинающему разработчику
Что такое type inference — аналогия с детективом
Представьте, что вы — детектив. Вы приходите на место происшествия и видите на столе чашку с ещё горячим кофе. Вы не знаете, кто её поставил, но можете сделать вывод: «Здесь кто-то был совсем недавно». Вы использовали доступные вам улики, чтобы определить ситуацию.
TypeScript работает точно так же! Когда вы пишете let возраст = 25, TypeScript не ждёт, что вы скажете ему «это число». Он сам смотрит на значение 25, анализирует его и делает вывод: «Ага, 25 — это число, значит переменная возраст имеет тип number».
Этот процесс называется type inference — неявное определение типа. TypeScript «выводит» тип из контекста, не требуя от вас явного указания. Это одна из самых мощных и удобных возможностей TypeScript, которая делает его гораздо проще в использовании, чем вы могли бы ожидать.
Давайте разберёмся подробнее. Без TypeScript вы могли бы писать на JavaScript так:
let имя = "Анна";
let возраст = 25;
let активна = true;
console.log(имя); // "Анна"
console.log(возраст); // 25
console.log(активна); // true
В JavaScript вы вообще не указываете типы — всё работает без проблем. Но вы и не защищены от ошибок. TypeScript же берёт тот же самый код и автоматически определяет типы:
let имя = "Анна"; // TS понимает: это string
let возраст = 25; // TS понимает: это number
let активна = true; // TS понимает: это boolean
// TypeScript теперь знает типы и может проверять код:
возраст = "двадцать пять"; // ❌ Ошибка! string нельзя присвоить number
Видите? Вы даже не написали ни одного двоеточия, но TypeScript уже защищает ваш код от ошибок. Это и есть магия type inference!
Аналогия: Type inference — это как автоматическое распознавание речи на вашем телефоне. Вы говорите, а телефон сам догадывается, что вы имели в виду. Вам не нужно каждый раз печатать слова по буквам — телефон умный enough, чтобы понять вас по контексту.
Как TypeScript определяет тип из присваиваемого значения
Самый простой и распространённый случай inference — это когда вы присваиваете значение переменной при объявлении. TypeScript смотрит на правую часть выражения и определяет тип.
Примитивные типы
Начнём с самых простых случаев — примитивных значений:
// Строка — TypeScript видит кавычки и понимает: это string
let сообщение = "Привет, мир!";
// TS inference: тип переменной — string
// Число — TypeScript видит цифры и понимает: это number
let цена = 99.99;
// TS inference: тип переменной — number
// Булево значение — TypeScript видит true/false
let включён = true;
// TS inference: тип переменной — boolean
// null — TypeScript видит null
let пусто = null;
// TS inference: тип переменной — null
// undefined — TypeScript видит undefined
let результат = undefined;
// TS inference: тип переменной — undefined
// BigInt
let большое = 100n;
// TS inference: тип переменной — bigint
// Символ
let уникальный = Symbol("id");
// TS inference: тип переменной — symbol
Обратите внимание на одну важную деталь. Когда вы пишете let, TypeScript выводит наиболее широкий (общий) тип. А когда вы пишете const, он выводит более узкий (литеральный) тип:
// С let — тип шире (number), потому что можно изменить
let возраст = 25;
// inference: number (не "25", а именно number)
// С const — тип уже (литерал 25), потому что нельзя изменить
const код = 25;
// inference: 25 (литеральный тип — конкретное значение)
// Разница важна!
возраст = 30; // ✅ ОК — number можно заменить на любое число
код = 30; // ❌ Ошибка! тип "25" несовместим с "30"
Совет преподавателя: В 99% случаев вас будет интересовать поведение с let. Литеральные типы — это продвинутая тема, о которой мы поговорим в более поздних уроках. Пока просто запомните: let даёт широкий тип, const — узкий.
Пустой объект и массив
Если вы создаёте пустой объект или массив, TypeScript не может определить тип содержимого и использует общий тип:
// Пустой объект — тип: {}
let данные = {};
// inference: {} (пустой объект, пока ничего не известно о его структуре)
// Пустой массив — тип: any[]
let список = [];
// inference: any[] (массив чего угодно, пока неизвестно содержимое)
// Пустой объект с присваиванием после
let пользователь = {};
пользователь = { имя: "Анна", возраст: 25 }; // ✅ Это сработает
Как видите, TypeScript не всегда может «угадать» тип, если значение недостаточно информативно. Это нормально — мы разберёмся, как с этим работать.
Значение выражений
TypeScript выводит тип не только из литералов, но и из результатов выражений:
// Результат арифметического выражения
let результат = 10 + 5; // inference: number (10 + 5 = 15, число)
// Результат конкатенации
let приветствие = "Привет" + ", " + "мир!"; // inference: string
// Результат сравнения
let проверка = 10 > 5; // inference: boolean (результат сравнения)
// Результат вызова parseInt
let число = parseInt("42"); // inference: number
// Результат вызова String()
let строка = String(123); // inference: string
// Тернарный оператор
let статус = 18 >= 18 ? "взрослый" : "ребёнок";
// inference: string (результат — одна из двух строк)
TypeScript очень умён в анализе выражений. Он «прослеживает» цепочку вычислений и определяет итоговый тип. Это значит, что вам редко нужно указывать типы вручную для простых операций.
Type inference с функциями — возвращаемый тип
Одна из самых полезных возможностей type inference — это автоматическое определение типа, который возвращает функция. Вы можете не указывать возвращаемый тип — TypeScript сам его выведет из тела функции.
Простые функции
// Функция возвращает число — TS сам выведет возвращаемый тип
function сложить(a: number, b: number) {
return a + b;
// TS inference: возвращаемый тип — number
}
// Используем функцию — TS знает, что результат это number
let сумма = сложить(5, 3); // inference: number
console.log(сумма * 2); // ✅ ОК — number можно умножать
// Функция возвращает строку
function приветствие(имя: string) {
return `Привет, ${имя}!`;
// TS inference: возвращаемый тип — string
}
let привет = приветствие("Анна"); // inference: string
console.log(привет.toUpperCase()); // ✅ ОК — string имеет метод toUpperCase()
// Функция возвращает булево значение
function являетсяЧётным(число: number) {
return число % 2 === 0;
// TS inference: возвращаемый тип — boolean
}
let проверка = являетсяЧётным(4); // inference: boolean
if (проверка) {
console.log("Число чётное!");
}
Функции с несколькими вариантами возврата
Когда функция может вернуть значения разных типов, TypeScript объединяет их в union-тип:
// Функция может вернуть строку или число
function найтиЭлемент(id: number) {
if (id === 0) {
return "не найдено"; // строка
}
return { id, название: "Элемент" }; // объект
// TS inference: возвращаемый тип — string | { id: number; название: string }
}
let результат1 = найтиЭлемент(0); // inference: string | { id: number; название: string }
let результат2 = найтиЭлемент(1); // inference: string | { id: number; название: string }
// Тип — объединение (union), потому что функция может вернуть разное
// Поэтому нужно проверять тип перед использованием:
if (typeof результат1 === "string") {
console.log(результат1.toUpperCase()); // ✅ ОК — это строка
} else {
console.log(результат1.название); // ✅ ОК — это объект
}
Важно: Когда функция возвращает значения разных типов, TypeScript выводит union-тип. Это значит, что вы не сможете просто так использовать результат — сначала нужно проверить, какой конкретно тип вернула функция. Это одна из ситуаций, когда inference работает «правильно», но вам нужно быть осторожными.
Стрелочные функции (анонимные)
// Стрелочная функция — inference тоже работает
const умножить = (а: number, б: number) => {
return а * б;
// TS inference: возвращаемый тип — number
};
let результат = умножить(4, 5); // inference: number
console.log(результат); // 20
// Стрелочная функция без тела (неявный возврат)
const квадрат = (число: number) => число * число;
// TS inference: возвращаемый тип — number
// Стрелочная функция, возвращающая строку
const форматировать = (число: number) => `Число: ${число}`;
// TS inference: возвращаемый тип — string
// Использование в callback
const числа = [1, 2, 3, 4, 5];
const удвоенные = числа.map(число => число * 2);
// inference: умноженные — number[]
console.log(удвоенные); // [2, 4, 6, 8, 10]
Когда возвращаемый тип — void
Если функция ничего не возвращает (нет оператора return или return без значения), TypeScript выводит тип void:
// Функция ничего не возвращает
function вывестиВКонсоль(сообщение: string) {
console.log(сообщение);
// Нет return — TS inference: возвращаемый тип — void
}
// Функция с пустым return
function раннийВыход() {
return;
// TS inference: возвращаемый тип — void
}
// Попытка использовать результат void-функции
let результат = вывестиВКонсоль("Привет");
// inference: результат имеет тип void
// результат.toUpperCase(); // ❌ Ошибка! У void нет методов
Type inference с массивами
Массивы — это ещё одна область, где type inference работает превосходно. TypeScript смотрит на элементы массива и определяет тип его содержимого.
Однотипные массивы
// Массив чисел — TS видит все числа и выводит number[]
const цены = [100, 200, 300, 400, 500];
// inference: number[]
// Теперь TS защищает массив от неправильных типов
цены.push(600); // ✅ ОК — число
// цены.push("шестьсот"); // ❌ Ошибка! string несовместим с number[]
// Массив строк
const имена = ["Анна", "Борис", "Вика"];
// inference: string[]
// Методы массива работают с правильными типами
let первое = имена[0]; // inference: string
let длина = имена.length; // inference: number
let большие = имена.filter(имя => имя.length > 3); // inference: string[]
// Массив булевых значений
const активные = [true, false, true, true];
// inference: boolean[]
// Смешанный пример — цепочки методов
const результаты = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0) // number[]
.map(n => n * 10); // number[]
// inference: результаты — number[]
console.log(результаты); // [20, 40, 60, 80, 100]
Массивы с начальными значениями
Тип массива определяется его начальными элементами. Если в массиве только числа — это number[]. Если только строки — string[].
// Только числа — inference: number[]
const оценки = [5, 4, 5, 3, 4, 5];
оценки.push(4); // ✅ ОК
// оценки.push("отлично"); // ❌ Ошибка
// Только строки — inference: string[]
const города = ["Москва", "Санкт-Петербург", "Казань");
города.push("Новосибирск"); // ✅ ОК
// города.push(123); // ❌ Ошибка
// Только булевы — inference: boolean[]
const флаги = [true, false, true];
// inference: boolean[]
// Пустой массив с типом any[]
constвременный = [];
// inference: any[] — пока ничего не известно
временный.push(1); // ✅ ОК
временный.push("строка"); // ✅ ОК — any[] принимает что угодно
// Но это опасно! Лучше указать тип явно в таких случаях
Вложенные массивы
// Массив массивов чисел
const матрица = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// inference: number[][]
// Доступ к элементу вложенного массива
let элемент = матрица[0][1]; // inference: number
console.log(элемент); // 2
// Массив массивов строк
const списки = [
["яблоко", "банан"],
["морковь", "огурец"]
];
// inference: string[][]
Совет: Если вы создаёте пустой массив и потом будете добавлять в него значения определённого типа, лучше указать тип явно: const числа: number[] = []. Это поможет TypeScript лучше понять ваш намерения и защитит от ошибок.
Type inference с объектами
TypeScript превосходно справляется с объектами. Он анализирует структуру объекта и выводит тип его свойств автоматически.
Простые объекты
// Объект с именем и возрастом
const пользователь = {
имя: "Анна",
возраст: 25,
активна: true
};
// inference: { имя: string; возраст: number; активна: boolean }
// TypeScript знает типы всех свойств!
console.log(пользователь.имя.toUpperCase()); // ✅ ОК — имя это string
console.log(пользователь.возраст.toFixed(2)); // ✅ ОК — возраст это number
// Ошибка — пытаемся изменить тип свойства
// пользователь.возраст = "двадцать пять"; // ❌ Ошибка! string несовместим с number
// Объект с вложенными свойствами
const заказ = {
id: 1,
товар: "Ноутбук",
цена: 50000,
доставка: {
город: "Москва",
дней: 3
}
};
// inference: { id: number; товар: string; цена: number; доставка: { город: string; дней: number } }
console.log(заказ.доставка.город); // ✅ ОК — TypeScript знает вложенную структуру
// заказ.доставка.город = 123; // ❌ Ошибка! string несовместим с number
Методы объектов
TypeScript выводит типы не только свойств, но и методов объектов, включая их возвращаемые типы:
// Объект с методами
const калькулятор = {
результат: 0,
сложить(а: number, б: number) {
return а + б; // inference: возвращаемый тип — number
},
вычесть(а: number, б: number) {
return а - б; // inference: возвращаемый тип — number
},
поздороваться(имя: string) {
return `Привет, ${имя}!`; // inference: возвращаемый тип — string
}
};
// inference: {
// результат: number;
// сложить: (а: number, б: number) => number;
// вычесть: (а: number, б: number) => number;
// поздороваться: (имя: string) => string;
// }
let сумма = калькулятор.сложить(5, 3); // inference: number
let привет = калькулятор.поздороваться("Анна"); // inference: string
Массив объектов
// Массив объектов — TS выводит тип каждого элемента
const студенты = [
{ имя: "Анна", оценка: 5 },
{ имя: "Борис", оценка: 4 },
{ имя: "Вика", оценка: 5 }
];
// inference: { имя: string; оценка: number }[]
// TypeScript знает структуру каждого элемента
студенты.forEach(студент => {
console.log(`${студент.имя}: ${студент.оценка}`);
// студент.имя — string, студент.оценка — number
});
// Фильтрация с правильными типами
let отличники = студенты.filter(студент => студент.оценка === 5);
// inference: отличники — { имя: string; оценка: number }[]
let имена = студенты.map(студент => студент.имя);
// inference: имена — string[]
console.log(имена); // ["Анна", "Борис", "Вика"]
Опциональные свойства в объектах
// Если свойство может быть undefined, TS выводит union-тип
const профиль = {
имя: "Анна",
возраст: 25,
email: undefined // явно undefined
};
// inference: { имя: string; возраст: number; email: undefined }
// Но лучше использовать опциональные свойства:
interface Профиль {
имя: string;
возраст: number;
email?: string; // опциональное
}
const пользователь2: Профиль = {
имя: "Борис",
возраст: 30
// email не указан — это нормально
};
// TS знает, что email может быть undefined
console.log(пользователь2.email?.toUpperCase());
// Оператор ?. безопасно обрабатывает undefined
Type inference с параметрами функций
Важный момент: TypeScript не выводит типы параметров функций автоматически. Это осознанное решение — чтобы функции были более гибкими. Но есть исключения.
Параметры callbacks
Когда функция передаётся как callback, TypeScript может вывести типы её параметров из контекста:
// Массив чисел — .forEach() знает, что элемент это number
const числа = [1, 2, 3, 4, 5];
// TS выводит тип параметра 'число' из контекста forEach
числа.forEach(число => {
console.log(число * 2); // ✅ TS знает что число — number
});
// То же самое с .map()
const строки = ["а", "б", "в"];
// TS выводит тип 'буква' из контекста map
const большие = строки.map(буква => буква.toUpperCase());
// inference: большие — string[]
// То же с .filter()
const фильтр = числа.filter(n => n > 3);
// TS знает что n — number из контекста filter
// inference: фильтр — number[]
// Пример с объектами
const студенты = [
{ имя: "Анна", оценка: 5 },
{ имя: "Борис", оценка: 4 }
];
// TS выводит тип параметра 'студент' из контекста
студенты
.filter(студент => студент.оценка >= 4) // студент — { имя: string; оценка: number }
.map(студент => студент.имя); // студент — { имя: string; оценка: number }
// inference: string[]
Когда inference параметров НЕ работает
// Простая функция — TypeScript НЕ выводит тип параметра
function добавить(а, б) {
// ❌ Ошибка! Параметр 'а' имеет неявный тип 'any'
return а + б;
}
// Нужно указать типы параметров явно
function добавить2(а: number, б: number) {
return а + б; // ✅ ОК
}
// Но если функция — callback, TypeScript выведет типы:
const числа2 = [1, 2, 3];
// ✅ TS выводит типы параметров из контекста
числа2.forEach((число, индекс) => {
// число — number, индекс — number
console.log(`Элемент ${индекс}: ${число}`);
});
// ✅ TS выводит тип для .reduce()
const сумма = числа2.reduce((аккумулятор, текущий) => {
return аккумулятор + текущий;
}, 0);
// inference: сумма — number
Почему так? TypeScript не выводит типы параметров обычных функций, потому что функция может быть вызвана с разными аргументами. Если бы TS угадал тип, это могло бы привести к неожиданным ошибкам. Для callback-функций контекст уже известен (массив, метод массива), поэтому TS смело выводит типы.
Собственные функции с callback
// Вы можете создать функцию, которая принимает callback,
// и TypeScript выведет типы параметров callback
function обработатьМассив<T>(массив: T[], callback: (элемент: T) => void) {
массив.forEach(callback);
}
// TS выводит тип T из массива [1, 2, 3] — это number
// Затем выводит тип параметра 'число' в callback
обработатьМассив([1, 2, 3], число => {
console.log(число * 2); // ✅ TS знает что число — number
});
// TS выводит тип T из ["а", "б"] — это string
обработатьМассив(["а", "б"], буква => {
console.log(буква.toUpperCase()); // ✅ TS знает что буква — string
});
// Ещё пример — функция с callback
function найтиПервый<T>(массив: T[], условие: (элемент: T) => boolean): T | undefined {
return массив.find(условие);
}
const студенты = [
{ имя: "Анна", оценка: 5 },
{ имя: "Борис", оценка: 3 }
];
// TS выводит тип параметра 'студент' из контекста
let отличник = найтиПервый(студенты, студент => студент.оценка === 5);
// inference: отличник — { имя: string; оценка: number } | undefined
if (отличник) {
console.log(отличник.имя); // ✅ ОК
}
Когда inference работает хорошо и когда нет
Type inference — мощный инструмент, но он не идеален. Давайте разберём ситуации, когда он работает превосходно, и когда лучше указывать тип явно.
Когда inference работает отлично
// 1. Примитивные типы — inference идеален
let имя = "Анна"; // string ✅
let возраст = 25; // number ✅
let активен = true; // boolean ✅
// 2. Возвращаемый тип функции — inference идеален
function получитьimet() {
return Date.now(); // number ✅
}
// 3. Массивы с однотипными элементами — inference идеален
const числа = [1, 2, 3]; // number[] ✅
// 4. Объекты с понятной структурой — inference идеален
const конфиг = {
хост: "localhost",
порт: 3000
}; // { хост: string; порт: number } ✅
// 5. Callback-функции в методах массива — inference идеален
[1, 2, 3].map(n => n * 2); // n — number ✅
// 6. Деструктуризация — inference работает
const [а, б, в] = [1, 2, 3]; // а, б, в — number ✅
// 7. Тернарный оператор
let x = true ? "да" : "нет"; // string ✅
// 8. Результаты методов строк
let длина = "привет".length; // number ✅
// 9. Результаты методов массивов
let первый = [1, 2, 3].pop(); // number | undefined ✅
// 10. Шаблонные строки
let сообщение = `Привет, ${имя}!`; // string ✅
Когда inference НЕ работает хорошо
// 1. Пустой массив — TS не знает тип элементов
const пустойМассив = [];
// inference: any[] — опасно!
// пустойМассив.push(1); // ОК
// пустойМассив.push("строка"); // ОК — это плохо!
// Решение: указать тип явно
const правильныйПустой: number[] = [];
// 2. Пустой объект — TS не знает структуру
const пустойОбъект = {};
// inference: {} — ничего не известно о свойствах
// 3. Функция без типов параметров
function обработать(а, б) {
// ❌ а и б — тип any
return а + б;
}
// 4. Асинхронные функции — может быть сложно
async function загрузить() {
const ответ = await fetch("/api");
// inference: ответ — Response, но JSON может быть чем угодно
const данные = await ответ.json();
// inference: данные — any (опасно!)
}
// 5. Динамические данные (из API, localStorage и т.д.)
let данные = JSON.parse("{\"имя\": \"Анна\"}");
// inference: данные — any (опасно!)
// 6. Результаты بعض методов
let результат = [1, "два", 3].find(x => typeof x === "string");
// inference: string | number | undefined — не очень точно
// 7. Функции, которые могут вернуть разные типы
function получитьЭлемент(id: number) {
if (id === 0) return null;
return { id, данные: "..." };
}
// inference: null | { id: number; данные: string }
// Неудобно работать с таким типом
Правило большого пальца: Если TypeScript выводит тип any — это сигнал, что нужно указать тип явно. Если выводит узкий, конкретный тип — можно доверять inference.
Лучшие практики
Зная, как работает inference, давайте определим, когда стоит ему доверять, а когда лучше указывать тип явно.
Доверяйте inference для:
// ✅ ДОВЕРЯЙТЕ inference когда:
// 1. Значение очевидно
let имя = "Анна"; // не нужно писать: let имя: string = "Анна"
let возраст = 25; // не нужно писать: let возраст: number = 25
let активна = true; // не нужно писать: let активна: boolean = true
// 2. Возвращаемый тип ясен
function получитьimet() {
return Date.now(); // не нужно писать: (): number
}
// 3. Вы используете методы массива
[1, 2, 3].map(n => n * 2); // не нужно писать: (n: number) => number
[1, 2, 3].filter(n => n > 1); // не нужно писать: (n: number) => boolean
// 4. Объект имеет понятную структуру
const пользователь = {
имя: "Анна",
возраст: 25
}; // inference работает превосходно
Указывайте тип явно для:
// ⚠️ УКАЗЫВАЙТЕ тип явно когда:
// 1. Пустой массив
const числа: number[] = []; // ✅ Явный тип
const пользователи: Пользователь[] = []; // ✅ Явный тип
// 2. Параметры функции (не callback)
function создатьЗапись(данные: Запись) { // ✅ Явный тип
// ...
}
// 3. Возвращаемый тип сложной функции
function получитьРасчёт(вход: ВходныеДанные): Расчёт {
// ... сложная логика
}
// 4. Промежуточная переменная для читаемости
const результат: number | null = вычислитьЧто_то();
// Читателю сразу видно тип
// 5. Когда inference выводит union, а вы знаете точный тип
let данные = получитьДанные(); // inference: Данные | null
let точныеДанные = данные as Данные; // вы знаете что это Данные
// 6. Публичные API / библиотечные функции
// Всегда указывайте типы возвращаемых значений для экспортируемых функций
export function получитьПользователя(id: number): Promise<Пользователь> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
Золотое правило: Не дублируйте тип там, где TypeScript уже всё понимает. Но всегда указывайте тип там, где inference не может или выводит слишком общий тип (any, unknown, void). Баланс — ключ к чистому коду.
Паттерны использования inference
// Паттерн 1: Цепочки методов — inference работает идеально
const результат = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0) // number[]
.map(n => n * n) // number[]
.reduce((а, б) => а + б, 0); // number
// inference: результат — number ✅
// Паттерн 2: Объекты как конфигурация
const конфиг = {
базаДанных: {
хост: "localhost",
порт: 5432,
имя: "mydb"
},
сервер: {
порт: 3000,
режим: "production"
}
};
// inference: вся структура выведена автоматически ✅
// Паттерн 3: Callback в Promise
const промис = new Promise<number>((resolve) => {
setTimeout(() => resolve(42), 1000);
});
промис.then(число => {
console.log(число * 2); // ✅ TS знает что число — number
});
// Паттерн 4: Map и Set
const карта = new Map<string, number>([
["яблоко", 5],
["банан", 3]
]);
// TS выводит типы для методов Map
let количество = карта.get("яблоко"); // inference: number | undefined
карта.set("апельсин", 7); // ✅ TS знает типы
// Паттерн 5: Деструктуризация
const { имя, возраст } = { имя: "Анна", возраст: 25 };
// inference: имя — string, возраст — number ✅
const [первый, ...остальные] = [1, 2, 3, 4, 5];
// inference: первый — number, остальные — number[] ✅
Ошибки при неправильном inference
Давайте посмотрим на типичные ошибки, которые возникают из-за неправильного понимания type inference, и как их избежать.
Ошибка 1: Пустой массив как any[]
// ❌ ПРОБЛЕМА: Пустой массив — тип any[]
const числа = [];
числа.push(1); // ОК
числа.push("строка"); // ОК — но это же массив чисел!
// ✅ РЕШЕНИЕ: Указать тип явно
const правильныеЧисла: number[] = [];
правильныеЧисла.push(1); // ✅ ОК
// правильныеЧисла.push("строка"); // ❌ Ошибка — хорошо!
// Или использовать generic
const genericsЧисла = new Array<number>();
genericsЧисла.push(1); // ✅ ОК
// genericsЧисла.push("строка"); // ❌ Ошибка — хорошо!
Ошибка 2: Параметры функции без типов
// ❌ ПРОБЛЕМА: TypeScript НЕ выводит типы параметров
function сложить(а, б) {
return а + б; // TS не знает типы — будет ошибка
}
// ✅ РЕШЕНИЕ: Указать типы параметров
function сложить2(а: number, б: number): number {
return а + б; // ✅ Всё ясно
}
// ✅ Или использовать типизацию через interface
interface Слагаемые {
первое: number;
второе: number;
}
function сложить3({ первое, второе }: Слагаемые): number {
return первое + второе; // ✅ Структурировано и ясно
}
Ошибка 3: Слишком широкий union-тип
// ❌ ПРОБЛЕМА: Функция возвращает слишком широкий тип
function найтиЭлемент(id: number) {
if (id === 0) return null;
return { id, данные: "..." };
}
// inference: null | { id: number; данные: string }
// Придётся проверять тип каждый раз
// ✅ РЕШЕНИЕ 1: Использовать throw вместо null
function найтиЭлемент2(id: number) {
if (id === 0) throw new Error("Элемент не найден");
return { id, данные: "..." };
}
// inference: { id: number; данные: string } — удобнее!
// ✅ РЕШЕНИЕ 2: Указать возвращаемый тип явно
interface Элемент {
id: number;
данные: string;
}
function найтиЭлемент3(id: number): Элемент | null {
if (id === 0) return null;
return { id, данные: "..." };
}
// Теперь возвращаемый тип чёткий и понятный
Ошибка 4: JSON.parse возвращает any
// ❌ ПРОБЛЕМА: JSON.parse возвращает any
interface Пользователь {
имя: string;
возраст: number;
}
const json = '{"имя": "Анна", "возраст": 25}';
const пользователь = JSON.parse(json);
// inference: any — опасно!
// ✅ РЕШЕНИЕ 1: Type assertion (as)
const пользователь2 = JSON.parse(json) as Пользователь;
// inference: Пользователь
// ✅ РЕШЕНИЕ 2: Присвоить переменной с типом
const пользователь3: Пользователь = JSON.parse(json);
// inference: Пользователь
// ✅ РЕШЕНИЕ 3: Generic
const пользователь4 = JSON.parse<Пользователь>(json);
// inference: Пользователь
// ✅ РЕШЕНИЕ 4: Валидация (самый безопасный способ)
function безопасныйParse(json: string): Пользователь | null {
try {
const данные = JSON.parse(json);
if (typeof данные.имя === "string" && typeof данные.возраст === "number") {
return данные;
}
return null;
} catch {
return null;
}
}
Ошибка 5: Неправильное использование const и let
// ❌ ПРОБЛЕМА: const с литеральным типом
const порт = 3000;
// inference: 3000 (литеральный тип!)
// порт = 8080; // ❌ Ошибка! Тип "3000" несовместим с "8080"
// ✅ РЕШЕНИЕ: Использовать let для переменных
let порт = 3000;
// inference: number
порт = 8080; // ✅ ОК
// ✅ ИЛИ: Явно указать тип
const порт2: number = 3000;
// inference: number
// порт2 = 8080; // ❌ Всё равно ошибка — const!
Типичная ошибка новичков: Многие думают, что TypeScript «угадывает» типы так же, как угадывает значения. Это не так. TypeScript анализирует код статически — до его выполнения. Он не запускает ваш код, чтобы посмотреть, что произойдёт. Он смотрит на структуру и выводит типы на основе правил.
Инструменты VS Code для просмотра типа
VS Code — лучший друг TypeScript-разработчика. Он показывает выведенные типы прямо в редакторе, и вот как этим пользоваться.
Hover (наведение курсора)
Просто наведите курсор на переменную, функцию или выражение — VS Code покажет выведенный тип:
// Наведите курсор на каждую переменную — увидите тип:
let имя = "Анна";
// Hover: let имя: string
let возраст = 25;
// Hover: let возраст: number
const массив = [1, 2, 3];
// Hover: const массив: number[]
function сложить(a: number, b: number) {
return a + b;
}
// Hover над функцией: function сложить(a: number, b: number): number
const объект = { ключ: "значение", число: 42 };
// Hover: const объект: { ключ: string; число: number }
Go to Type Definition
Нажмите Cmd + Click (Mac) или Ctrl + Click (Windows) на типе, чтобы перейти к его определению:
interface Пользователь {
id: number;
имя: string;
email: string;
}
function создатьПользователя(данные: Пользователь) {
// Cmd+Click на "Пользователь" покажет определение интерфейса
console.log(данные.имя);
}
Панель TypeScript Problems
Откройте панель Problems (Cmd+Shift+M) — все ошибки типа будут перечислены там с описанием проблемы.
Проверка типа через JavaScript
Вы можете использовать TypeScript для проверки типов даже в JavaScript-проектах, создав файл .ts рядом:
// В JavaScript файле (app.js):
let пользователь = { имя: "Анна", возраст: 25 };
// Создайте проверочный файл (app.check.ts):
// import { пользователь } from "./app";
// Теперь наведите курсор на пользователь — увидите тип
Команда tsc для проверки
// В терминале VS Code можно проверить типы:
// npx tsc --noEmit
// Эта команда проверит все .ts файлы на ошибки типа
// без создания .js файлов
// Если хотите видеть выведенные типы, используйте:
// npx tsc --declaration --emitDeclarationOnly
// Это создаст .d.ts файлы с определениями типов
Совет: В VS Code можно настроить автоподсказки типов. Откройте настройки (Cmd+,), найдите "TypeScript: Inlay Hints" и включите их. Вы увидите выведенные типы прямо в коде — это очень помогает при работе с inference!
Практические примеры: полные сценарии
Давайте рассмотрим несколько полных примеров, которые покажут, как inference работает в реальных сценариях.
Сценарий 1: Работа с формой
// Данные формы — inference работает с объектами
const данные формы = {
имя: "Анна",
email: "anna@example.com",
возраст: 25,
активна: true
};
// inference: { имя: string; email: string; возраст: number; активна: boolean }
// Валидация с использованием inference
function валидироватьФорму(данные: typeof данные формы) {
const ошибки: string[] = [];
if (!данные.имя) {
ошибки.push("Имя обязательно");
}
if (!данные.email.includes("@")) {
ошибки.push("Некорректный email");
}
if (данные.возраст < 18) {
ошибки.push("Возраст должен быть ≥ 18");
}
return {
валидно: ошибки.length === 0,
ошибки
};
}
let результат = валидироватьФорму(данные формы);
// inference: результат — { валидно: boolean; ошибки: string[] }
console.log(результат.валидно); // ✅ TS знает тип свойства
Сценарий 2: Обработка данных из API
// Типы данных
interface Товар {
id: number;
название: string;
цена: number;
категория: string;
}
// Функция обработки — inference работает с возвращаемым типом
function обработатьТовары(товары: Товар[]) {
return товары
.filter(товар => товар.цена > 1000) // inference: Товар[]
.map(товар => ({
...товар,
цена: товар.цена * 0.9 // скидка 10%
})); // inference: Товар[]
}
// Функция группировки
function сгруппироватьПоКатегории(товары: Товар[]) {
return товары.reduce((группы, товар) => {
const категория = товар.категория;
if (!группы[категория]) {
группы[категория] = [];
}
группы[категория].push(товар);
return группы;
}, {} as Record<string, Товар[]>);
// inference: Record<string, Товар[]>
}
// Использование
const товары: Товар[] = [
{ id: 1, название: "Ноутбук", цена: 50000, категория: "Электроника" },
{ id: 2, название: "Книга", цена: 500, категория: "Литература" },
{ id: 3, название: "Телефон", цена: 30000, категория: "Электроника" }
];
let обработанные = обработатьТовары(товары);
// inference: обработанные — Товар[]
console.log(обработанные[0].цена); // ✅ TS знает тип
let сгруппированные = сгруппироватьПоКатегории(товары);
// inference: сгруппированные — Record<string, Товар[]>
console.log(сгруппированные["Электроника"]); // ✅ ОК
Сценарий 3: Состояние приложения
// Состояние приложения — inference работает с объектами
const состояние = {
пользователь: null as { имя: string; роль: string } | null,
загрузка: false,
ошибка: null as string | null,
данные: [] as { id: number; название: string }[]
};
// inference: {
// пользователь: { имя: string; роль: string } | null;
// загрузка: boolean;
// ошибка: string | null;
// данные: { id: number; название: string }[]
// }
// Функции обновления состояния
function начатьЗагрузку() {
состояние.загрузка = true;
состояние.ошибка = null;
}
function завершитьЗагрузку(данные: { id: number; название: string }[]) {
состояние.загрузка = false;
состояние.данные = данные;
}
function показатьОшибку(сообщение: string) {
состояние.загрузка = false;
состояние.ошибка = сообщение;
}
// Проверка состояния
function получитьСтатус(): string {
if (состояние.загрузка) return "Загрузка...";
if (состояние.ошибка) return `Ошибка: ${состояние.ошибка}`;
return `Загружено ${состояние.данные.length} элементов`;
}
console.log(получитьСтатус()); // ✅ Всё типизировано
Сценарий 4: Утилиты и хелперы
// Утилита для группировки массива по ключу
function сгруппировать<T, K extends keyof T>(
массив: T[],
ключ: K
): Record<string, T[]> {
return массив.reduce((группы, элемент) => {
const значение = String(элемент[ключ]);
if (!группы[значение]) {
группы[значение] = [];
}
группы[значение].push(элемент);
return группы;
}, {} as Record<string, T[]>);
}
const студенты = [
{ имя: "Анна", курс: 1, оценка: 5 },
{ имя: "Борис", курс: 2, оценка: 4 },
{ имя: "Вика", курс: 1, оценка: 5 },
{ имя: "Глеб", курс: 2, оценка: 3 }
];
// TS выводит K = "курс", T[] = студенты[]
let поКурсам = сгруппировать(студенты, "курс");
// inference: Record<string, { имя: string; курс: number; оценка: number }[]>
console.log(поКурсам["1"]); // [{имя: "Анна", ...}, {имя: "Вика", ...}]
console.log(поКурсам["2"]); // [{имя: "Борис", ...}, {имя: "Глеб", ...}]
// Утилита для поиска
function найтиПервыйПоУсловию<T>(
массив: T[],
условие: (элемент: T) => boolean
): T | undefined {
return массив.find(условие);
}
let отличник = найтиПервыйПоУсловию(студенты, s => s.оценка === 5);
// inference: отличник — { имя: string; курс: number; оценка: number } | undefined
Взаимодействие inference с другими фичами TypeScript
Type inference не живёт в изоляции — он тесно связан с другими возможностями TypeScript. Давайте посмотрим, как они работают вместе.
Inference и Union-типы
// TS выводит union-тип когда значение может быть разным
let данные = Math.random() > 0.5 ? "строка" : 42;
// inference: string | number
// TypeScript не позволит использовать union без проверки
// данные.toUpperCase(); // ❌ Ошибка — number не имеет toUpperCase
// данные.toFixed(2); // ❌ Ошибка — string не имеет toFixed
// Нужно сначала проверить тип:
if (typeof данные === "string") {
console.log(данные.toUpperCase()); // ✅ ОК
} else {
console.log(данные.toFixed(2)); // ✅ ОК
}
// Union-типы с литералами
function получитьЦвет(): "красный" | "зелёный" | "синий" {
const цвета = ["красный", "зелёный", "синий"];
return цвета[Math.floor(Math.random() * 3)] as "красный" | "зелёный" | "синий";
}
let цвет = получитьЦвет(); // inference: "красный" | "зелёный" | "синий"
// switch с автоматическим сужением типов
switch (цвет) {
case "красный":
console.log("Тревога!"); break;
case "зелёный":
console.log("Всё хорошо!"); break;
case "синий":
console.log("Спокойствие"); break;
}
Inference и Generics
// Generics — это когда TypeScript выводит тип "на лету"
function перв элемент<T>(массив: T[]): T | undefined {
return массив[0];
}
// TS выводит T = number
let первоеЧисло =перв элемент([1, 2, 3]);
// inference: первоеЧисло — number | undefined
// TS выводит T = string
let перваяСтрока =перв элемент(["а", "б", "в"]);
// inference: перваяСтрока — string | undefined
// TS выводит T = { имя: string; возраст: number }
let первыйПользователь =перв элемент([
{ имя: "Анна", возраст: 25 },
{ имя: "Борис", возраст: 30 }
]);
// inference: первыйПользователь — { имя: string; возраст: number } | undefined
// Ещё пример с несколькими generic-параметрами
function пары<A, B>(а: A, б: B): [A, B] {
return [а, б];
}
let перваяПара = пары("привет", 42);
// inference: перваяПара — [string, number]
let втораяПара = пары(true, { ключ: "значение" });
// inference: втораяПара — [boolean, { ключ: string }]
Inference и Type Guards
// Type guard — функция, которая "сужает" тип
interface Кот {
тип: "кот";
имя: string;
лает: () => void;
}
interface Собака {
тип: "собака";
имя: string;
мяукает: () => void;
}
type Животное = Кот | Собака;
// Type guard
function являетсяКотом(животное: Животное): животное is Кот {
return животное.тип === "кот";
}
const мкот: Кот = { тип: "кот", имя: "Мурзик", лает: () => {} };
const собака: Собака = { тип: "собака", имя: "Шарик", мяукает: () => {} };
function поговорить(животное: Животное) {
if (являетсяКотом(животное)) {
// TS теперь знает что животное — Кот!
животное.лает(); // ✅ ОК
console.log(`${животное.имя} — это кот`);
} else {
// TS теперь знает что животное — Собака!
животное.мяукает(); // ✅ ОК
console.log(`${животное.имя} — это собака`);
}
}
Inference и Template Literal Types
// TS может вывести шаблонные типы строк
type Статус = "активен" | "неактивен";
type Роль = "админ" | "пользователь";
// Шаблонный литеральный тип
type Ключ = `${Роль}_${Статус}`;
// inference: "админ_активен" | "админ_неактивен" | "пользователь_активен" | "пользователь_неактивен"
function получить ключ(роль: Роль, статус: Статус): Ключ {
return `${роль}_${статус}`;
}
let ключ = получить ключ("админ", "активен");
// inference: ключ — "админ_активен"
// В реальном коде это может быть полезно для:
// - формирования CSS-классов
// - формирования строк запросов
// - создания уникальных идентификаторов
Сравнение: с inference и без
Давайте наглядно сравним код с использованием inference и без него.
// ❌ БЕЗ inference — многословно и избыточно
const имя2: string = "Анна";
const возраст2: number = 25;
const активна2: boolean = true;
const числа2: number[] = [1, 2, 3];
const пользователь2: { имя: string; возраст: number } = {
имя: "Анна",
возраст: 25
};
function сложить2(а: number, б: number): number {
return а + б;
}
// ✅ С inference — лаконично и чисто
let имя = "Анна"; // string из inference
let возраст = 25; // number из inference
let активна = true; // boolean из inference
const числа = [1, 2, 3]; // number[] из inference
const пользователь = {
имя: "Анна",
возраст: 25
}; // тип выведен автоматически
function сложить(а: number, б: number) {
return а + б; // возвращаемый тип выведен автоматически
}
// Разница в читаемости огромна!
// Код с inference чище, короче и так же безопасен
Вывод: Type inference делает код чище и удобнее, не жертвуя безопасностью. Используйте его, когда TypeScript может надёжно определить тип. Указывайте тип явно, когда inference не справляется или выводит слишком общий тип.
Часто задаваемые вопросы
Q: Всегда ли inference работает правильно?
A: В 90% случаев — да. Но есть ситуации, где inference выводит слишком общий тип (any, unknown) или union-тип. В таких случаях лучше указать тип явно. Вывод всегда правильный с точки зрения TypeScript, но не всегда удобный для разработчика.
Q: Влияет ли inference на производительность?
A: Нет! Type inference работает только во время компиляции. В скомпилированном JavaScript-коде нет никаких следов TypeScript — типы полностью исчезают. Производительностьво время выполнения не меняется.
Q: Нужно ли всегда указывать типы явно?
A: Нет! Это одна из ошибок новичков. TypeScript — это не Java. Доверяйте inference там, где он работает хорошо. Указывайте типы явно для параметров функций, возвращаемых типов сложных функций, пустых массивов и данных из внешних источников (API, JSON).
Q: Что такое "type narrowing"?
A: Type narrowing — это когда TypeScript автоматически сужает (уточняет) тип в определённых контекстах. Например, после проверки typeof или if, TypeScript "знает", что переменная имеет определённый тип. Это тесно связано с inference.
Q: Как посмотреть выведенный тип в IDE?
A: В VS Code наведите курсор на переменную — всплывающая подсказка покажет тип. Также можно использовать Inlay Hints для отображения типов прямо в коде. В IntelliJ IDEA —такжеподдерживает hover и type hints.
Упражнения для закрепления
Упражнение 1: Определите inference-типы
Посмотрите на код ниже и определите, какие типы выведет TypeScript:
// Какие типы выведет TS?
let a = "привет"; // ?
let b = 42; // ?
let c = [1, 2, 3]; // ?
let d = { x: 1, y: "а" }; // ?
let e = true ? 1 : "а"; // ?
let f = [1, "два", true]; // ?
Ответы: string, number, number[], { x: number; y: string }, number | string, (number | string | boolean)[]
Упражнение 2: Исправьте ошибки inference
// Исправьте ошибки, используя правильный тип явно:
// 1. Пустой массив чисел
const числа = []; // Ошибка: any[]
numbers.push(1); // Это опасно!
// 2. Функция без типов параметров
function найтиMax(а, б) {
return а > б ? а : б;
}
// 3. Объект с неправильным типом свойства
const конфиг = {
порт: "3000" // Должно быть number!
};
Упражнение 3: Напишите функцию с inference
Напишите функцию, которая использует type inference, а не явные типы:
// Функция должна принимать массив объектов и ключ,
// и возвращать массив значений по этому ключу
// Используйте generics для type inference!
function получитьЗначения<T, K extends keyof T>(
массив: T[],
ключ: K
): T[K][] {
// Ваш код здесь
}
// Использование:
const пользователи = [
{ имя: "Анна", возраст: 25 },
{ имя: "Борис", возраст: 30 }
];
let имена = получитьЗначения(пользователи, "имя");
// inference: имена — string[]
let возрасты = получитьЗначения(пользователи, "возраст");
// inference: возрасты — number[]
Итоги урока
Type inference — это механизм, с помощью которого TypeScript автоматически определяет типы переменных, возвращаемых значений и других выражений на основе контекста.
- Как работает: TypeScript анализирует присваиваемое значение и выводит наиболее подходящий тип. Чем точнее значение, тем точнее вывод.
- Где работает отлично: Примитивы, массивы с однотипными элементами, объекты с понятной структурой, возвращаемые типы функций, callback-функции в методах массива.
- Где не работает: Пустые массивы, параметры обычных функций, данные из JSON.parse, переменные с динамическими значениями.
- Лучшие практики: Доверяйте inference для простых случаев. Указывайте тип явно для параметров функций, возвращаемых типов, пустых массивов и публичных API.
- Инструменты: Hover в VS Code, Inlay Hints, Go to Type Definition, панель Problems.
- let vs const:
letдаёт широкий тип,const— узкий литеральный.
Помните: type inference — ваш друг. Он делает код чище и безопаснее одновременно. Но не бойтесь указывать тип явно, когда это необходимо — TypeScript не против, он только приветствует!
Продолжение обучения
В следующем уроке мы изучим явное указание типа — когда и как писать типы вручную, когда это действительно нужно, и как совмещать inference с явной типизацией для максимальной безопасности кода.
Проверяем понимание
8 вопросов