Hack Frontend Community

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)