Звуження типів у TypeScript
Що таке звуження типів?
Звуження типів — це процес, за допомогою якого 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));
}
}Способи звуження типів
Захист за допомогою typeof
Перевірка примітивних типів за допомогою 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 — це об'єкт!)
console.log(value); // може бути null
}
}Захист за допомогою instanceof
Перевірка належності до класу.
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
}
}Оператор in
Перевірка наявності пропси в об'єкті.
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
}
}Звуження через рівність
Звуження через перевірку рівності.
function process(x: string | number, y: string | boolean) {
if (x === y) {
// x і y можуть бути рівними лише якщо обидва є рядками
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
}
}Звуження за правдивістю
Звуження на основі перевірки правдивості/хибності.
function printLength(str: string | null | undefined) {
if (str) {
// str: string (null і undefined видалені)
console.log(str.length);
}
}Хибні значення
function process(value: string | number | null | undefined | 0 | '') {
if (value) {
// value: string | number (хибні значення видалені)
// АЛЕ! 0 і '' також є хибними, тому їх також видалено
}
}Увага:
Звуження правдивості видаляє ВСІ хибні значення: 0, '', false, null, undefined, NaN.
Більш точна перевірка
function processValue(value: string | null) {
if (value !== null) {
// value: string
console.log(value.length);
}
}Предикати типу (is)
Користувацькі захисти типу з ключовим словом 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}!`);
}
}Дискриміновані об'єднання
Звуження на основі спільної дискримінуючої пропси.
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
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;
}
}Звуження при присвоєнні
Звуження при присвоєнні.
let value: string | number;
value = 'hello';
// value: string (звужено до string)
console.log(value.toUpperCase());
value = 42;
// value: number (звужено до number)
console.log(value.toFixed(2));Аналіз потоку управління
TypeScript аналізує потік виконання коду.
function process(value: string | null) {
if (value === null) {
return;
}
// value: string (null виключено після return)
console.log(value.toUpperCase());
}Викидання виключень
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 (після підтвердження)
console.log(value.toUpperCase());
}Практичні приклади
Обробка відповіді 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);
}Валідація полів форми
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;
}Робота з подіями
function handleEvent(event: MouseEvent | KeyboardEvent) {
if (event instanceof MouseEvent) {
console.log(`Mouse: ${event.clientX}, ${event.clientY}`);
} else {
console.log(`Key: ${event.key}`);
}
}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 та вичерпність
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 };
// Помилка у випадку за замовчуванням!Обмеження звуження типів
Зміни змінних у колбек-ах
function process(value: string | null) {
if (value !== null) {
setTimeout(() => {
// Помилка! value могло змінитися
console.log(value.toUpperCase());
}, 1000);
}
value = null; // Змінено!
}Мутації об'єктів
interface Container {
value: string | number;
}
function process(container: Container) {
if (typeof container.value === 'string') {
setTimeout(() => {
// Помилка! value могло змінитися
console.log(container.value.toUpperCase());
}, 0);
}
}Найкращі практики
Використовуйте захисти типу для складних перевірок
// Погано
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);
}
}Раннє повернення для спрощення
// Погано
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);
// багато коду...
}Використовуйте дискриміновані об'єднання
// Погано
interface Result {
success: boolean;
data?: string;
error?: string;
}
// Добре
type Result =
| { success: true; data: string }
| { success: false; error: string };Висновок
Звуження типів:
- Автоматичне уточнення типу на основі перевірок
typeofдля примітивівinstanceofдля класівinдля властивостей об'єктів- Звуження через рівність і правдивість
- Предикати типу (
is) для користувацьких перевірок - Дискриміновані об'єднання для станів
- Аналіз потоку управління відстежує виконання
- Never для перевірки вичерпності
На співбесідах:
Важливо вміти:
- Пояснити концепцію звуження типів
- Показати різні методи звуження (typeof, instanceof, in)
- Написати захист типу з
is - Пояснити дискриміновані об'єднання
- Показати перевірку вичерпності через never
- Обговорити обмеження (колбеки, мутації)
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.