Перейти к содержанию
Educora
Продвинутый20 мин8 / 8

Современные приёмы и файлы объявлений

Узнай, что ловит строгий режим, получай точные типы с помощью `as const` и `satisfies` и типизируй сторонние библиотеки JavaScript файлами объявлений `.d.ts`.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять, какие проверки включает режим strict
  • Получать точные литеральные типы из значений через as const
  • Сравнивать satisfies с аннотацией типа и с as
  • Пользоваться пакетами @types и писать простой файл .d.ts

Ты уже знаешь типы, функции, интерфейсы, объединения, классы и дженерики. Этот последний урок — о четырёх инструментах, которыми профессионалы пользуются каждый день: строгий режим, который ловит большую часть ошибок; as const, чтобы получать точные типы из значений; satisfies, который проверяет тип, не расширяя его; и файлы объявлений, которые знакомят TypeScript с библиотеками JavaScript без типов.

Строгий режим

"strict": true в tsconfig.json — это не один параметр, а целый набор проверок. Без него TypeScript на многое закрывает глаза, и самые частые ошибки JavaScript проскальзывают. Вот важнейшие проверки из этого набора:

  • strictNullChecks — null и undefined становятся отдельными типами; значение, которое «может быть undefined», нельзя использовать без проверки.
  • noImplicitAny — параметр, тип которого нельзя определить, не становится молча any, а вызывает ошибку.
  • strictPropertyInitialization — каждое поле класса должно получить значение при объявлении или в конструкторе.
  • useUnknownInCatchVariables — в catch (error) переменная error имеет тип unknown, а не any.
TypeScript
const users = [
  { name: 'Aysel', age: 15 },
  { name: 'Murad', age: 17 },
];

const found = users.find((u) => u.name === 'Leyla');
console.log(found.age);
Text
users.ts:7:13 - error TS18048: 'found' is possibly 'undefined'.

7 console.log(found.age);
              ~~~~~
find может ничего не найти. Без строгого режима код скомпилировался бы без ошибок, а при выполнении упал бы с TypeError: Cannot read properties of undefined (reading 'age').
TypeScript
const users = [
  { name: 'Aysel', age: 15 },
  { name: 'Murad', age: 17 },
];

const found = users.find((u) => u.name === 'Leyla');
console.log(found?.age ?? 'not found');
Ожидаемый результат
not found
Правильное решение: обработать случай «не найдено» через ?. и ??.

as const

as const просит считать значение «замороженным»: строки получают точные литеральные типы вместо string, массив становится кортежем только для чтения, а свойства объекта — readonly. Это идеально подходит для фиксированных списков вроде ролей, статусов и настроек и удобно заменяет enum, о котором мы говорили раньше:

TypeScript
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number];

function canEdit(role: Role): boolean {
  return role !== 'viewer';
}

const Status = { Active: 'active', Blocked: 'blocked' } as const;
type Status = (typeof Status)[keyof typeof Status];

const s: Status = Status.Blocked;
console.log(ROLES.join(', '), canEdit('editor'), s);
Ожидаемый результат
admin, editor, viewer true blocked
Тип Role — это 'admin' | 'editor' | 'viewer', а тип Status — 'active' | 'blocked'. Список существует и в программе: его можно вывести и перебрать в цикле.
TypeScript
const ROLES = ['admin', 'editor', 'viewer'] as const;
ROLES.push('guest');
Text
roles.ts:2:7 - error TS2339: Property 'push' does not exist on type 'readonly ["admin", "editor", "viewer"]'.

2 ROLES.push('guest');
        ~~~~
as const превращает массив в кортеж только для чтения, поэтому метода push у него нет.

Оператор satisfies

Аннотация типа (const x: T = ...) проверяет значение, но расширяет тип переменной до T, и точные сведения о значении теряются. А satisfies T только проверяет: значение должно подходить под T, а переменная сохраняет точный тип, выведенный TypeScript. В палитре одни цвета — строки, другие — кортежи из трёх чисел:

TypeScript
type ColorName = 'red' | 'green' | 'blue';
type Color = string | [number, number, number];

const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255],
} satisfies Record<ColorName, Color>;

console.log(palette.green.toUpperCase());
console.log(palette.red[0]);
Ожидаемый результат
#00FF00
255
TypeScript знает, что green — строка, а red — кортеж, потому что satisfies сохраняет точный тип.
TypeScript
type ColorName = 'red' | 'green' | 'blue';
type Color = string | [number, number, number];

const annotated: Record<ColorName, Color> = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255],
};
annotated.green.toUpperCase();

const checked = {
  red: [255, 0, 0],
  green: '#00ff00',
  bleu: [0, 0, 255],
} satisfies Record<ColorName, Color>;
Text
palette.ts:9:17 - error TS2339: Property 'toUpperCase' does not exist on type 'Color'.
  Property 'toUpperCase' does not exist on type '[number, number, number]'.

9 annotated.green.toUpperCase();
                  ~~~~~~~~~~~

palette.ts:14:3 - error TS2353: Object literal may only specify known properties, and 'bleu' does not exist in type 'Record<ColorName, Color>'.

14   bleu: [0, 0, 255],
     ~~~~
Первая ошибка: после аннотации green — просто Color. Вторая: satisfies по-прежнему проверяет значение и ловит опечатку bleu.
ЗаписьПроверяет?Тип переменной
const x: T = valueдаT (расширяется)
const x = value satisfies Tдаточный тип значения
const x = value as Tпочти нетT

Файлы объявлений и сторонние библиотеки

Определение
Файл объявлений (.d.ts)

Файл, в котором есть только типы и нет исполняемого кода. Это «инструкция» к коду на JavaScript для TypeScript: какие функции существуют, какие параметры принимают и что возвращают.

Типы для npm-пакетов появляются одним из трёх способов. Многие современные пакеты поставляются вместе со своими файлами .d.ts, и делать ничего не нужно. Для других сообщество публикует типы в отдельных пакетах @types/.... Если нет ни того, ни другого, файл объявлений пишут самостоятельно.

Terminal
npm install lodash
npm install --save-dev @types/lodash
Сама библиотека попадает в dependencies, а её типы — в devDependencies, потому что нужны только при разработке.

Допустим, в проекте есть старая библиотека old-slug без типов — она превращает текст в короткое имя для URL. При её импорте TypeScript предупреждает, что не нашёл типы модуля:

Text
src/index.ts:1:21 - error TS7016: Could not find a declaration file for module 'old-slug'. '/home/leyla/blog/node_modules/old-slug/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/old-slug` if it exists or add a new declaration (.d.ts) file containing `declare module 'old-slug';`

1 import slugify from 'old-slug';
                      ~~~~~~~~~~

Если пакета @types/old-slug нет, создаём файл types/old-slug.d.ts и описываем модуль через declare module. Слово declare означает, что этот код уже существует в другом месте, а мы лишь сообщаем его тип:

TypeScript
declare module 'old-slug' {
  export default function slugify(text: string, separator?: string): string;
}
types/old-slug.d.ts — сигнатура функции без тела, только типы.
TypeScript
import slugify from 'old-slug';

const title = 'Hello from Baku';
console.log(slugify(title));
console.log(slugify(title, '_'));
Ожидаемый результат
hello-from-baku
hello_from_baku
src/index.ts теперь проходит проверку, редактор подсказывает параметры, а вызов slugify(42) дал бы ошибку TS2345.

Когда публикуешь собственную библиотеку, писать .d.ts-файлы вручную не нужно. С параметром "declaration": true в tsconfig.json tsc создаёт рядом с каждым .js-файлом его .d.ts:

TypeScript
export interface User {
    name: string;
    age: number;
}
export declare function greet(user: User): string;
greet.d.ts, созданный из greet.ts: остались интерфейс и сигнатура функции, а тело находится в greet.js.

Главное

  • strict: true включает strictNullChecks, noImplicitAny и другие проверки; держи его всегда включённым.
  • as и ! прячут сообщение об ошибке, но не решают проблему.
  • as const делает из значения точные литеральные типы только для чтения; (typeof LIST)[number] выводит тип-объединение из списка.
  • satisfies T проверяет значение по T, но сохраняет точный тип переменной.
  • Файлы .d.ts содержат только типы; типы берутся из самого пакета, из пакетов @types/... или из файла, который ты пишешь с declare module.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что делает "strict": true в tsconfig.json?