TypeScript справжнім чином змінив розробку на JavaScript, привнісши строгі типи та передові шаблони в екосистему. У цьому посібнику ми розглянемо просунуті патерни TypeScript, які не лише роблять ваш код більш надійним, але й підвищують ваші загальні навички розробки на JavaScript.
Зміст
- Програмування на рівні типів
- Просунуті шаблони з використанням Generic
- Дискриміновані об'єднання
- Глибоке занурення в утилітарні типи
- Фабричні патерни
- Патерни керування станом
- Патерни будівника
- Приклади з реального світу
1. Програмування на рівні типів
Умовні типи
// Просунутий шаблон умовного типу
type IsArray = T extends Array ? true : false;
type IsString = T extends string ? true : false;
// Практичний приклад: обробник відповіді API
type ApiResponse = {
data: T;
status: number;
message: string;
};
type ExtractData = T extends ApiResponse ? U : never;
// Приклад використання
interface UserData {
id: number;
name: string;
}
type UserApiResponse = ApiResponse;
type ExtractedUserData = ExtractData; // Повертає тип UserData
Типи шаблонів літералів
// Визначення допустимих HTTP методів
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiEndpoint = '/users' | '/posts' | '/comments';
// Створення типів маршрутів API
type ApiRoute = `${HttpMethod} ${ApiEndpoint}`;
// Перевірка маршрутів під час компіляції
const validRoute: ApiRoute = 'GET /users'; // ✅ Дозволено
const invalidRoute: ApiRoute = 'PATCH /users'; // ❌ Помилка типу
2. Просунуті шаблони з використанням Generic
Фабрика з обмеженнями для Generic
interface HasId {
id: number;
}
interface HasTimestamps {
createdAt: Date;
updatedAt: Date;
}
// Генерична фабрика з обмеженнями
class EntityFactory {
create(data: Omit): T {
return {
...data,
createdAt: new Date(),
updatedAt: new Date()
} as T;
}
update(entity: T, data: Partial>): T {
return {
...entity,
...data,
updatedAt: new Date()
};
}
}
// Приклад використання
interface User extends HasId, HasTimestamps {
id: number;
name: string;
email: string;
}
const userFactory = new EntityFactory();
const user = userFactory.create({ id: 1, name: 'John', email: '[email protected]' });
Безпечний для типів "Emitter" подій
type EventMap = {
'user:login': { userId: string; timestamp: number };
'user:logout': { userId: string; timestamp: number };
'error': { message: string; code: number };
}
class TypedEventEmitter> {
private listeners: Partial> = {};
on(event: K, callback: (data: T[K]) => void) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
emit(event: K, data: T[K]) {
this.listeners[event]?.forEach(callback => callback(data));
}
}
// Приклад використання
const emitter = new TypedEventEmitter();
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`Користувач ${userId} увійшов о ${timestamp}`);
});
// Безпечний для типів emit
emitter.emit('user:login', {
userId: '123',
timestamp: Date.now()
});
## Дискриміновані об'єднання
## Патерн керування станом
// Визначення можливих станів з дискримінованим об'єднанням
type AsyncState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E };
// Генеричний обробник асинхронних даних
class AsyncData {
private state: AsyncState = { status: 'idle' };
setState(newState: AsyncState) {
this.state = newState;
this.render();
}
render() {
switch (this.state.status) {
case 'idle':
console.log('Очікування початку...');
break;
case 'loading':
console.log('Завантаження...');
break;
case 'success':
console.log('Дані:', this.state.data);
break;
case 'error':
console.log('Помилка:', this.state.error);
break;
}
}
}
// Приклад використання
interface UserProfile {
id: string;
name: string;
}
const userProfile = new AsyncData();
```
4. Глибоке занурення в утилітарні типи
Просунуті відображені типи
// Зробити всі властивості необов'язковими і nullable
type Nullable = { [P in keyof T]: T[P] | null };
// Зробити певні властивості обов'язковими
type RequiredProps = T & { [P in K]-?: T[P] };
// Глибокий partial тип
type DeepPartial = {
[P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
};
// Приклад використання
interface Config {
api: {
baseUrl: string;
timeout: number;
headers: {
authorization: string;
contentType: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial;
const config: PartialConfig = {
api: {
baseUrl: 'https://api.example.com',
headers: {
authorization: 'Bearer token'
}
}
};
5. Фабричні патерни
Абстрактний фабричний патерн
// Абстрактні інтерфейси продуктів
interface Button {
render(): void;
onClick(): void;
}
interface Input {
render(): void;
getValue(): string;
}
// Абстрактний інтерфейс фабрики
interface UIFactory {
createButton(): Button;
createInput(): Input;
}
// Конкретні реалізації
class MaterialButton implements Button {
render() { console.log('Відображення кнопки Material'); }
onClick() { console.log('Кнопка Material натиснута'); }
}
class MaterialInput implements Input {
render() { console.log('Відображення поля вводу Material'); }
getValue() { return 'Значення поля вводу Material'; }
}
class MaterialUIFactory implements UIFactory {
createButton(): Button {
return new MaterialButton();
}
createInput(): Input {
return new MaterialInput();
}
}
// Використання з ін'єкцією залежностей
class Form {
constructor(private factory: UIFactory) {}
render() {
const button = this.factory.createButton();
const input = this.factory.createInput();
button.render();
input.render();
}
}
6. Патерни керування станом
Безпечний для типів патерн Redux
// Типи дій з дискримінованими об'єднаннями
type Action =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'TOGGLE_TODO'; payload: { id: number } }
| { type: 'DELETE_TODO'; payload: { id: number } };
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface State {
todos: Todo[];
loading: boolean;
}
// Безпечний для типів редуктор
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload.text,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
}
}
## Патерни будівників
## Патерн флюїдного будівника
class QueryBuilder {
private query: Partial = {};
private conditions: Array<(item: T) => boolean> = [];
where(key: K, value: T[K]): this {
this.query[key] = value;
return this;
}
whereIn(key: K, values: T[K][]): this {
this.conditions.push((item: T) =>
values.includes(item[key])
);
return this;
}
build(): (item: T) => boolean {
const query = this.query;
const conditions = this.conditions;
return (item: T) => {
const matchesQuery = Object.entries(query).every(
([key, value]) => item[key as keyof T] === value
);
const matchesConditions = conditions.every(
condition => condition(item)
);
return matchesQuery && matchesConditions;
};
}
}
// Приклад використання
interface User {
id: number;
name: string;
age: number;
role: 'admin' | 'user';
}
const query = new QueryBuilder()
.where('role', 'admin')
.whereIn('age', [25, 30, 35])
.build();
const users: User[] = [
{ id: 1, name: 'John', age: 30, role: 'admin' },
{ id: 2, name: 'Jane', age: 25, role: 'user' }
];
const results = users.filter(query);
```
8. Приклади з реального світу
Безпечний для типів API-клієнт
// Визначення кінцевих точок API
interface ApiEndpoints {
'/users': {
GET: {
response: User[];
query: { role?: string };
};
POST: {
body: Omit;
response: User;
};
};
'/users/:id': {
GET: {
params: { id: string };
response: User;
};
PUT: {
params: { id: string };
body: Partial;
response: User;
};
};
}
// Безпечний для типів API-клієнт
class ApiClient {
async get<
Path extends keyof ApiEndpoints,
Method extends keyof ApiEndpoints[Path],
Endpoint extends ApiEndpoints[Path][Method]
>(
path: Path,
config?: {
params?: Endpoint extends { params: any } ? Endpoint['params'] : never;
query?: Endpoint extends { query: any } ? Endpoint['query'] : never;
}
): Promise {
// Реалізація
return {} as any;
}
async post<
Path extends keyof ApiEndpoints,
Method extends keyof ApiEndpoints[Path],
Endpoint extends ApiEndpoints[Path][Method]
>(
path: Path,
body: Endpoint extends { body: any } ? Endpoint['body'] : never
): Promise {
// Реалізація
return {} as any;
}
}
// Приклад використання
const api = new ApiClient();
// Безпечні для типів API-виклики
const users = await api.get('/users', { query: { role: 'admin' } });
const user = await api.post('/users', { name: 'John', age: 30, role: 'user' });
Висновок
Ці просунуті патерни TypeScript можуть значно покращити якість вашого коду та досвід розробника. Пам'ятайте про такі рекомендації:
- Починайте з простих патернів і поступово вводьте складність
- Використовуйте типову систему TypeScript для запобігання помилок на етапі компіляції
- Використовуйте обмеження для узагальнених типів для створення багаторазових компонентів
- Документуйте складні типові патерни для кращого розуміння вашою командою
Показані патерни — це лише початок, типова система TypeScript неймовірно потужна і може бути використана для створення ще більш складних патернів, що відповідають вашим конкретним потребам.
Бажаєте дізнатися більше такого контенту?
Слідкуйте за мною або напишіть мені на Linkedin.
Перекладено з: Advanced TypeScript Patterns That Will Make You a Better JavaScript Developer