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

Тип any и unknown в TypeScript

Понимание «сбежавших» типов, безопасный any и структурированный unknown

Добро пожаловать!

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

Цели урока

1
Понять, что такое тип any и почему он опасен
2
Изучить тип unknown как безопасную альтернативу
3
Научиться безопасно приводить unknown к конкретному типу
4
Определить ситуации, когда any допустим

Что такое тип any?

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

Пример: Поведение типа any

let значение: any = "Привет";

// Комплилятор позволяет ВСЁ:
значение = 42;                // OK: any принимает число
значение = true;              // OK: any принимает булево
значение = { x: 1, y: 2 };   // OK: any принимает объект

// Методы any:
console.log(значение.toUpperCase());  // Нет ошибки!
console.log(значение.toFixed(2));     // Нет ошибки!
console.log(значение.map(x => x));   // Нет ошибки!

Почему any опасен?

В приведённом выше примере значение — это строка, но TypeScript не запрещает вызвать .toFixed(2) (метод числа) или .map() (метод массива). Ошибка возникнет только во время выполнения программы. Именно это делает any «сбежавшим» из-под контроля типов — компилятор не может вас защитить.

Понятие «сбежавший тип» (type escape)

Когда вы присваиваете значение типа any переменной конкретного типа, «заражение» распространяется дальше по коду. Если функция возвращает any, все её вызовы становятся потенциально небезопасными. Именно поэтому TypeScript рекомендует избегать any там, где это возможно.

Неявный any (implicit any)

Помимо явного указания any, существует более скрытая проблема — неявный any. Он возникает, когда TypeScript не может вывести тип и автоматически присваивает any.

Примеры неявного any

// Параметр функции без аннотации типа
// TypeScript не знает, что здесь должно быть
function сложить(a, b) {    // a и b — implicit any!
  return a + b;
}

// Переменная без инициализации и аннотации
let результат;              // implicit any!

// Колбэк без типизации
массив.forEach((элемент) => {
  // элемент — implicit any!
});

// Правильно:
function сложить(a: number, b: number): number {
  return a + b;
}

Настройка noImplicitAny

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

Отличие явного any от неявного

Явный any — вы сами написали : any. Это ваш осознанный выбор. Неявный any — компилятор сам сделал предположение, что переменная имеет тип any, потому что вы не указали тип. Неявный any гораздо опаснее, потому что вы можете даже не подозревать, что потеряли типобезопасность.

Что такое тип unknown?

Тип unknown — это безопасная версия any. Переменная типа unknown может хранить любое значение, как и any, но в отличие от any, вы не можете выполнять никаких операций над ней без предварительной проверки типа.

Пример: Поведение типа unknown

let значение: unknown = "Привет";

// Присваивать — можно (как и any):
значение = 42;
значение = true;
значение = { x: 1, y: 2 };

// Но использовать — НЕЛЬЗЯ без проверки:
// console.log(значение.toUpperCase());  // Ошибка компиляции!
// console.log(значение.toFixed(2));     // Ошибка компиляции!

// Безопасное использование — только после проверки типа:
if (typeof значение === "string") {
  console.log(значение.toUpperCase());  // OK: значение теперь string
}

if (typeof значение === "number") {
  console.log(значение.toFixed(2));     // OK: значение теперь number
}

Почему unknown безопаснее any?

unknown заставляет вас доказать тип значения перед использованием. Это называется сужающее приведение типов (type narrowing). Пока вы не выполнили проверку (typeof, instanceof, пользовательскую функцию-предикат), TypeScript запрещает любые операции. Вы не можете случайно вызвать несуществующий метод — компилятор поймает ошибку на этапе компиляции, а не во время выполнения.

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

any

  • Можно присваивать любые значения
  • Можно вызывать любые методы
  • Можно передавать куда угодно
  • Нет никакой проверки типов
  • Ошибка только во время выполнения
  • Отключает систему типов

unknown

  • Можно присваивать любые значения
  • Нельзя вызывать методы без проверки
  • Нельзя передавать куда угодно
  • Полная проверка типов
  • Ошибка на этапе компиляции
  • Сохраняет систему типов

Наглядный пример

// с any — ОШИБКА ТОЛЬКО ВО ВРЕМЯ ВЫПОЛНЕНИЯ:
let данные_any: any = "привет";
данные_any.toUpperCase();   // OK (компилятор молчит)
данные_any.nonExistent();   // OK (компилятор молчит) — CRASH при запуске!

// с unknown — ОШИБКА НА ЭТАПЕ КОМПИЛЯЦИИ:
let данные_unknown: unknown = "привет";
// данные_unknown.toUpperCase();          // ОШИБКА компиляции!

if (typeof данные_unknown === "string") {
  данные_unknown.toUpperCase();           // OK после проверки
}
// данные_unknown.nonExistent();          // ОШИБКА компиляции!

Сужение типов с unknown

Чтобы безопасно использовать значение типа unknown, нужно выполнить сужение типа (type narrowing) — последовательность проверок, которая убедит компилятор в реальном типе значения.

Способы 1: typeof

function обработать(значение: unknown): string {
  if (typeof значение === "string") {
    return значение.toUpperCase();
  }
  if (typeof значение === "number") {
    return значение.toFixed(2);
  }
  if (typeof значение === "boolean") {
    return значение ? "Да" : "Нет";
  }
  return String(значение);
}

console.log(обработать("привет"));    // "ПРИВЕТ"
console.log(обработать(3.14159));     // "3.14"
console.log(обработать(true));        // "Да"

Способ 2: instanceof

function обработатьОшибку(ошибка: unknown): string {
  if (ошибка instanceof Error) {
    return `Ошибка: ${ошибка.message}`;
  }
  if (typeof ошибка === "string") {
    return `Ошибка: ${ошибка}`;
  }
  return "Неизвестная ошибка";
}

Способ 3: Пользовательский предикат

interface Пользователь {
  id: number;
  имя: string;
  email: string;
}

function является_пользователем(значение: unknown): значение is Пользователь {
  return (
    typeof значение === "object" &&
    значение !== null &&
    "id" in значение &&
    "имя" in значение &&
    "email" in значение
  );
}

function обработатьДанные(данные: unknown) {
  if (является_пользователем(данные)) {
    console.log(`Пользователь: ${данные.имя}`);
    console.log(`Email: ${данные.email}`);
  }
}

Когда any всё-таки допустим?

Несмотря на все аргументы против any, бывают ситуации, когда его использование оправдано:

Допустимые случаи

  • Миграция с JavaScript на TypeScript
  • Работа с не типизированными библиотеками
  • Временный обход сложной проблемы типизации
  • Получение данных из внешних источников
  • Тестирование прототипов

Плохие причины

  • «Я не знаю тип»
  • «Типизировать слишком сложно»
  • «Раньше работало без типов»
  • «Это временно» (а потом забыл)
  • «Мне лень разбираться»

Правило: any — это аварийный выход

Используйте any только когда действительно нет другой возможности, и немедленно добавляйте комментарий // TODO: заменить any на конкретный тип. В идеале — в следующем же коммите. Если вы используете any в нескольких местах, это признак того, что пора пересмотреть архитектуру.

Практические примеры

Пример 1: Парсинг JSON

// JSON.parse() возвращает any — это опасно!
const сырые_данные: any = JSON.parse('{"имя":"Иван","возраст":30}');
console.log(сырые_данные.имя);        // OK, но компилятор не проверяет

// Безопасный вариант с unknown и проверкой:
interface Пользователь {
  имя: string;
  возраст: number;
}

function безопасный_парсинг(json: string): unknown {
  return JSON.parse(json);
}

const данные: unknown = безопасный_парсинг('{"имя":"Иван","возраст":30}');
if (
  typeof данные === "object" &&
  данные !== null &&
  "имя" in данные &&
  "возраст" in данные
) {
  const пользователь = данные as Пользователь;
  console.log(`Привет, ${пользователь.имя}!`);
}

Пример 2: Обработка ошибок из API

async function получить_данные() {
  try {
    const ответ = await fetch("https://api.example.com/data");
    const данные: unknown = await ответ.json();
    return данные;
  } catch (ошибка) {
    // ошибка имеет тип unknown в modern TypeScript
    if (ошибка instanceof Error) {
      console.error(`Ошибка: ${ошибка.message}`);
    } else {
      console.error("Неизвестная ошибка");
    }
    throw ошибка;
  }
}

Пример 3: Безопасная обёртка

// Универсальная функция проверки типа
function утверждение_типа(
  значение: unknown,
  предикат: (значение: unknown) => значение is T,
  имя_типа: string
): T {
  if (предикат(значение)) {
    return значение;
  }
  throw new Error(`Ожидался тип ${имя_типа}`);
}

// Использование:
interface Ответ {
  статус: number;
  тело: string;
}

function является_ответом(значение: unknown): значение is Ответ {
  return (
    typeof значение === "object" &&
    значение !== null &&
    "статус" in значение &&
    "тело" in значение
  );
}

const данные: unknown = JSON.parse('{"статус":200,"тело":"OK"}');
const ответ = утверждение_типа(данные, является_ответом, "Ответ");
console.log(ответ.статус);  // 200

Тип any vs unknown: матрица решений

// ПАТТЕРН 1: «Знаю тип, но он сложный»
const сложные_данные: any = получить_из_сторонней_библиотеки();
// ↓ Лучше: типизировать интерфейс
interface МоиДанные { /* ... */ }
const сложные_данные: МоиДанные = получить();

// ПАТТЕРН 2: «Не знаю тип на этапе компиляции»
const данные_из_json: unknown = JSON.parse(сырой_json);
// ↓ Проверяем и используем безопасно
if (typeof данные_из_json === "object" && данные_из_json !== null) {
  // ...
}

// ПАТТЕРН 3: «Заполнитель пока не написал типы»
let результат: any; // TODO: заменить на строку
// ↓ В следующем коммите:
let результат: string;

Золотое правило

Если вы можете использовать unknown вместо any — используйте unknown. Если вы можете типизировать значение конкретным типом — сделайте это. any — это крайняя мера, когда ни unknown, ни конкретный тип не подходят.

Подведение итогов

Что мы изучили

  • any отключает проверку типов полностью
  • unknown принимает любые значения, но требует проверку
  • Неявный any опаснее явного
  • Настройка noImplicitAny — must have
  • Сужение типа — ключ к безопасной работе с unknown

Ключевые выводы

  • any — последнее средство, не привычка
  • unknown — безопасная замена any
  • Проверяйте типы с typeof, instanceof
  • Используйте предикаты для сложных типов
  • Всегда стремитесь к конкретным типам

В следующем уроке мы изучим оставшиеся специальные типы TypeScript: void и never, которые дополняют картину «сбежавших» и «специальных» типов.

Тест по типам any и unknown

10 вопросов

Безопасное приведение unknown

Premium