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

Неявное определение типа (type inference)

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

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

Что такое 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 вопросов

Вывод типа из контекста

Premium