Type Narrowing в TypeScript
Что такое Type Narrowing?
Type Narrowing (Сужение типов) — это процесс, при котором TypeScript уточняет тип переменной из более общего к более конкретному на основе проверок в коде.
function process(value: string | number) {
// Здесь value: string | number
if (typeof value === 'string') {
// Здесь value: string (тип сужен!)
console.log(value.toUpperCase());
} else {
// Здесь value: number (остался только number)
console.log(value.toFixed(2));
}
}
Способы сужения типов
1. typeof Guard
Проверка примитивных типов через typeof.
function printValue(value: string | number | boolean) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
} else if (typeof value === 'number') {
console.log(value.toFixed(2));
} else {
console.log(value ? 'true' : 'false');
}
}
Важно:
typeof null возвращает 'object', это особенность JavaScript!
function process(value: string | null) {
if (typeof value === 'object') {
// Здесь value все еще string | null (null - это object!)
console.log(value); // может быть null
}
}
2. instanceof Guard
Проверка принадлежности к классу.
class Dog {
bark() {
console.log('Woof!');
}
}
class Cat {
meow() {
console.log('Meow!');
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animal: Dog
} else {
animal.meow(); // animal: Cat
}
}
Работа с встроенными классами
function processValue(value: Date | string) {
if (value instanceof Date) {
console.log(value.getFullYear()); // value: Date
} else {
console.log(value.toUpperCase()); // value: string
}
}
3. in Operator
Проверка наличия свойства в объекте.
interface Circle {
radius: number;
}
interface Square {
size: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if ('radius' in shape) {
// shape: Circle
return Math.PI * shape.radius ** 2;
} else {
// shape: Square
return shape.size ** 2;
}
}
Проверка методов
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly(); // animal: Bird
} else {
animal.swim(); // animal: Fish
}
}
4. Equality Narrowing
Сужение через проверку на равенство.
function process(x: string | number, y: string | boolean) {
if (x === y) {
// x и y могут быть равны только если оба string
console.log(x.toUpperCase()); // x: string
console.log(y.toUpperCase()); // y: string
}
}
Проверка на null и undefined
function printName(name: string | null | undefined) {
if (name !== null && name !== undefined) {
console.log(name.toUpperCase()); // name: string
}
// Или короче
if (name != null) {
console.log(name.toUpperCase()); // name: string
}
}
5. Truthiness Narrowing
Сужение на основе проверки на истинность/ложность.
function printLength(str: string | null | undefined) {
if (str) {
// str: string (убрали null и undefined)
console.log(str.length);
}
}
Falsy значения
function process(value: string | number | null | undefined | 0 | '') {
if (value) {
// value: string | number (убрали falsy значения)
// НО! 0 и '' тоже falsy, поэтому их тоже нет
}
}
Осторожно:
Truthiness narrowing убирает ВСЕ falsy значения: 0, '', false, null, undefined, NaN.
Более точная проверка
function processValue(value: string | null) {
if (value !== null) {
// value: string
console.log(value.length);
}
}
6. Type Predicates (is)
Пользовательские type guards с ключевым словом is.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(value: unknown) {
if (isString(value)) {
// value: string
console.log(value.toUpperCase());
}
}
Более сложные проверки
interface User {
name: string;
email: string;
}
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'name' in obj &&
'email' in obj &&
typeof (obj as User).name === 'string' &&
typeof (obj as User).email === 'string'
);
}
function greetUser(data: unknown) {
if (isUser(data)) {
// data: User
console.log(`Hello, ${data.name}!`);
}
}
7. Discriminated Unions
Сужение на основе общего дискриминирующего свойства.
type Success = {
status: 'success';
data: string;
};
type Error = {
status: 'error';
message: string;
};
type Result = Success | Error;
function handleResult(result: Result) {
if (result.status === 'success') {
// result: Success
console.log(result.data);
} else {
// result: Error
console.log(result.message);
}
}
Switch statement
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET'; value: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
// action: { type: 'INCREMENT' }
return state + 1;
case 'DECREMENT':
// action: { type: 'DECREMENT' }
return state - 1;
case 'SET':
// action: { type: 'SET'; value: number }
return action.value;
}
}
8. Assignment Narrowing
Сужение при присваивании.
let value: string | number;
value = 'hello';
// value: string (сужен до string)
console.log(value.toUpperCase());
value = 42;
// value: number (сужен до number)
console.log(value.toFixed(2));
9. Control Flow Analysis
TypeScript анализирует поток выполнения кода.
function process(value: string | null) {
if (value === null) {
return;
}
// value: string (null исключен после return)
console.log(value.toUpperCase());
}
Throw statements
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Not a string!');
}
}
function process(value: unknown) {
assertIsString(value);
// value: string (после assertion)
console.log(value.toUpperCase());
}
Практические примеры
1. Обработка API ответа
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
async function fetchUser(id: number): Promise<ApiResponse<User>> {
// ...
}
const response = await fetchUser(1);
if (response.success) {
// response: { success: true; data: User }
console.log(response.data.name);
} else {
// response: { success: false; error: string }
console.error(response.error);
}
2. Проверка полей формы
interface FormData {
name?: string;
email?: string;
age?: number;
}
function validateForm(data: FormData): boolean {
if (!data.name) {
console.error('Name is required');
return false;
}
// data.name: string (не undefined)
if (data.name.length < 3) {
console.error('Name too short');
return false;
}
if (!data.email) {
console.error('Email is required');
return false;
}
// data.email: string
if (!data.email.includes('@')) {
console.error('Invalid email');
return false;
}
return true;
}
3. Работа с событиями
function handleEvent(event: MouseEvent | KeyboardEvent) {
if (event instanceof MouseEvent) {
console.log(`Mouse: ${event.clientX}, ${event.clientY}`);
} else {
console.log(`Key: ${event.key}`);
}
}
4. Array.isArray()
function process(value: string | string[]) {
if (Array.isArray(value)) {
// value: string[]
value.forEach(item => console.log(item));
} else {
// value: string
console.log(value);
}
}
Never тип и exhaustiveness
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.size ** 2;
default:
// shape: never (все случаи обработаны)
const _exhaustive: never = shape;
return _exhaustive;
}
}
Если добавить новый тип, TypeScript выдаст ошибку:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'triangle'; base: number; height: number };
// Ошибка в default case!
Ограничения Type Narrowing
1. Изменение переменных в callback
function process(value: string | null) {
if (value !== null) {
setTimeout(() => {
// Ошибка! value может быть изменен
console.log(value.toUpperCase());
}, 1000);
}
value = null; // Изменили!
}
2. Мутация объектов
interface Container {
value: string | number;
}
function process(container: Container) {
if (typeof container.value === 'string') {
setTimeout(() => {
// Ошибка! value мог быть изменен
console.log(container.value.toUpperCase());
}, 0);
}
}
Best Practices
1. Используйте type guards для сложных проверок
// Плохо
function process(data: unknown) {
if (
typeof data === 'object' &&
data !== null &&
'name' in data &&
typeof (data as any).name === 'string'
) {
console.log((data as { name: string }).name);
}
}
// Хорошо
function isUser(data: unknown): data is { name: string } {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
typeof (data as any).name === 'string'
);
}
function process(data: unknown) {
if (isUser(data)) {
console.log(data.name);
}
}
2. Ранний выход для упрощения
// Плохо
function process(value: string | null) {
if (value !== null) {
console.log(value.toUpperCase());
console.log(value.length);
// много кода...
}
}
// Хорошо
function process(value: string | null) {
if (value === null) return;
// value: string во всей функции
console.log(value.toUpperCase());
console.log(value.length);
// много кода...
}
3. Используйте discriminated unions
// Плохо
interface Result {
success: boolean;
data?: string;
error?: string;
}
// Хорошо
type Result =
| { success: true; data: string }
| { success: false; error: string };
Вывод
Type Narrowing:
- Автоматическое сужение типов на основе проверок
typeofдля примитивовinstanceofдля классовinдля свойств объектов- Equality и truthiness narrowing
- Type predicates (
is) для кастомных проверок - Discriminated unions для состояний
- Control flow analysis отслеживает поток выполнения
- Never для exhaustiveness checking
На собеседовании:
Важно уметь:
- Объяснить концепцию type narrowing
- Показать разные способы сужения (typeof, instanceof, in)
- Написать type guard с
is - Объяснить discriminated unions
- Показать exhaustiveness checking через never
- Рассказать об ограничениях (callbacks, mutations)