Перейти к основному содержимому

TypeScript полезные сниппеты

Практические примеры TypeScript кода для ежедневной разработки.

Типы и интерфейсы

Базовые типы

// Union типы
type Status = 'loading' | 'success' | 'error';
type ID = string | number;

// Mapped типы
type Optional<T> = {
[K in keyof T]?: T[K];
};

type Readonly<T> = {
readonly [K in keyof T]: T[K];
};

// Conditional типы
type NonNullable<T> = T extends null | undefined ? never : T;

// Template literal типы
type EventName<T> = `on${Capitalize<T>}`;
type ButtonEvent = EventName<'click'>; // 'onClick'

Utility типы

// Pick и Omit
interface User {
id: number;
name: string;
email: string;
password: string;
}

type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
type UserInput = Omit<User, 'id'>;

// Partial и Required
type UserUpdate = Partial<User>;
type CompleteUser = Required<User>;

// Record
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

// ReturnType и Parameters
function createUser(name: string, email: string): User {
return { id: 1, name, email, password: '' };
}

type CreateUserReturn = ReturnType<typeof createUser>; // User
type CreateUserParams = Parameters<typeof createUser>; // [string, string]

Дженерики

// Базовые дженерики
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
total: number;
page: number;
limit: number;
}

// Дженерик функции
function createArray<T>(length: number, value: T): T[] {
return Array(length).fill(value);
}

// Constraint дженерики
interface Identifiable {
id: string | number;
}

function findById<T extends Identifiable>(items: T[], id: string | number): T | undefined {
return items.find(item => item.id === id);
}

// Conditional дженерики
type ApiResult<T> = T extends string
? { message: T }
: T extends number
? { code: T }
: { data: T };

Классы и декораторы

Классы с типизацией

abstract class BaseEntity {
abstract id: string;
createdAt: Date = new Date();
updatedAt: Date = new Date();

abstract validate(): boolean;

save(): void {
this.updatedAt = new Date();
console.log(`Saving ${this.constructor.name}`);
}
}

class User extends BaseEntity {
id: string;
name: string;
email: string;
private _password: string;

constructor(name: string, email: string, password: string) {
super();
this.id = crypto.randomUUID();
this.name = name;
this.email = email;
this._password = password;
}

validate(): boolean {
return this.email.includes('@') && this._password.length >= 8;
}

get password(): string {
return '*'.repeat(this._password.length);
}

set password(value: string) {
if (value.length < 8) {
throw new Error('Password must be at least 8 characters');
}
this._password = value;
}
}

Декораторы

// Декоратор логирования
function Log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;

descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyName} with args:`, args);
const result = method.apply(this, args);
console.log(`Result:`, result);
return result;
};
}

// Декоратор валидации
function Validate(validator: (value: any) => boolean, message: string) {
return function (target: any, propertyName: string) {
let value = target[propertyName];

const getter = () => value;
const setter = (newValue: any) => {
if (!validator(newValue)) {
throw new Error(message);
}
value = newValue;
};

Object.defineProperty(target, propertyName, {
get: getter,
set: setter
});
};
}

class Product {
@Validate((value) => value > 0, 'Price must be positive')
price: number;

constructor(price: number) {
this.price = price;
}

@Log
calculateDiscount(percentage: number): number {
return this.price * (percentage / 100);
}
}

Асинхронный код

Promise и async/await

// Типизированные Promise
interface ApiUser {
id: number;
name: string;
email: string;
}

class UserService {
private baseUrl = 'https://api.example.com';

async getUser(id: number): Promise<ApiUser> {
const response = await fetch(`${this.baseUrl}/users/${id}`);

if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`);
}

return response.json();
}

async getUsers(): Promise<ApiUser[]> {
const response = await fetch(`${this.baseUrl}/users`);
return response.json();
}

async createUser(userData: Omit<ApiUser, 'id'>): Promise<ApiUser> {
const response = await fetch(`${this.baseUrl}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});

return response.json();
}
}

// Обработка ошибок
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };

async function safeApiCall<T>(
apiCall: () => Promise<T>
): Promise<Result<T>> {
try {
const data = await apiCall();
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}

Обработка множественных Promise

// Promise.all с типизацией
async function fetchUserData(userId: number) {
const [user, posts, comments] = await Promise.all([
userService.getUser(userId),
postService.getUserPosts(userId),
commentService.getUserComments(userId)
]);

return { user, posts, comments };
}

// Promise.allSettled для надежности
async function fetchOptionalData(userId: number) {
const results = await Promise.allSettled([
userService.getUser(userId),
userService.getUserPreferences(userId),
userService.getUserStats(userId)
]);

const [userResult, prefsResult, statsResult] = results;

return {
user: userResult.status === 'fulfilled' ? userResult.value : null,
preferences: prefsResult.status === 'fulfilled' ? prefsResult.value : null,
stats: statsResult.status === 'fulfilled' ? statsResult.value : null
};
}

Работа с DOM

Event handling

// Типизированные обработчики событий
class TodoApp {
private todoList: HTMLUListElement;
private addButton: HTMLButtonElement;
private todoInput: HTMLInputElement;

constructor() {
this.todoList = document.getElementById('todo-list') as HTMLUListElement;
this.addButton = document.getElementById('add-btn') as HTMLButtonElement;
this.todoInput = document.getElementById('todo-input') as HTMLInputElement;

this.setupEventListeners();
}

private setupEventListeners(): void {
this.addButton.addEventListener('click', this.handleAddTodo.bind(this));
this.todoInput.addEventListener('keypress', this.handleKeyPress.bind(this));
this.todoList.addEventListener('click', this.handleTodoClick.bind(this));
}

private handleAddTodo(): void {
const text = this.todoInput.value.trim();
if (text) {
this.addTodo(text);
this.todoInput.value = '';
}
}

private handleKeyPress(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.handleAddTodo();
}
}

private handleTodoClick(event: MouseEvent): void {
const target = event.target as HTMLElement;

if (target.classList.contains('delete-btn')) {
const todoItem = target.closest('li');
todoItem?.remove();
} else if (target.classList.contains('todo-text')) {
target.classList.toggle('completed');
}
}

private addTodo(text: string): void {
const li = document.createElement('li');
li.innerHTML = `
<span class="todo-text">${text}</span>
<button class="delete-btn">Удалить</button>
`;
this.todoList.appendChild(li);
}
}

Custom Elements

interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}

class CustomButton extends HTMLElement {
private _props: ButtonProps = {};

constructor() {
super();
this.attachShadow({ mode: 'open' });
}

connectedCallback(): void {
this.render();
this.setupEventListeners();
}

static get observedAttributes(): string[] {
return ['variant', 'size', 'disabled'];
}

attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
this._props[name as keyof ButtonProps] = newValue as any;
this.render();
}

private render(): void {
const { variant = 'primary', size = 'medium', disabled = false } = this._props;

this.shadowRoot!.innerHTML = `
<style>
button {
padding: ${size === 'small' ? '0.25rem 0.5rem' : size === 'large' ? '0.75rem 1.5rem' : '0.5rem 1rem'};
background: ${variant === 'primary' ? '#007bff' : variant === 'danger' ? '#dc3545' : '#6c757d'};
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
<button ${disabled ? 'disabled' : ''}>
<slot></slot>
</button>
`;
}

private setupEventListeners(): void {
const button = this.shadowRoot!.querySelector('button')!;
button.addEventListener('click', (event) => {
if (!this._props.disabled) {
this.dispatchEvent(new CustomEvent('custom-click', {
detail: { originalEvent: event },
bubbles: true
}));
}
});
}
}

customElements.define('custom-button', CustomButton);

Паттерны проектирования

Observer Pattern

interface Observer<T> {
update(data: T): void;
}

class Observable<T> {
private observers: Observer<T>[] = [];

subscribe(observer: Observer<T>): void {
this.observers.push(observer);
}

unsubscribe(observer: Observer<T>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}

notify(data: T): void {
this.observers.forEach(observer => observer.update(data));
}
}

// Применение
interface UserData {
id: number;
name: string;
status: 'online' | 'offline';
}

class UserStatusDisplay implements Observer<UserData> {
update(userData: UserData): void {
console.log(`User ${userData.name} is now ${userData.status}`);
}
}

const userObservable = new Observable<UserData>();
const statusDisplay = new UserStatusDisplay();
userObservable.subscribe(statusDisplay);

Factory Pattern

interface Product {
name: string;
price: number;
category: string;
}

abstract class ProductFactory {
abstract createProduct(name: string, price: number): Product;

processOrder(name: string, price: number): Product {
const product = this.createProduct(name, price);
console.log(`Processing order for ${product.name}`);
return product;
}
}

class ElectronicsFactory extends ProductFactory {
createProduct(name: string, price: number): Product {
return {
name,
price,
category: 'Electronics'
};
}
}

class ClothingFactory extends ProductFactory {
createProduct(name: string, price: number): Product {
return {
name,
price,
category: 'Clothing'
};
}
}

// Использование
const electronicsFactory = new ElectronicsFactory();
const laptop = electronicsFactory.processOrder('Laptop', 999);

Singleton Pattern

class ConfigManager {
private static instance: ConfigManager;
private config: Record<string, any> = {};

private constructor() {}

static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}

get<T>(key: string): T | undefined {
return this.config[key];
}

set<T>(key: string, value: T): void {
this.config[key] = value;
}

has(key: string): boolean {
return key in this.config;
}
}

// Использование
const config = ConfigManager.getInstance();
config.set('apiUrl', 'https://api.example.com');
const apiUrl = config.get<string>('apiUrl');

Валидация и схемы

Простая система валидации

type ValidationRule<T> = (value: T) => string | null;

class Validator<T> {
private rules: ValidationRule<T>[] = [];

addRule(rule: ValidationRule<T>): this {
this.rules.push(rule);
return this;
}

validate(value: T): string[] {
return this.rules
.map(rule => rule(value))
.filter((error): error is string => error !== null);
}

isValid(value: T): boolean {
return this.validate(value).length === 0;
}
}

// Создание валидаторов
const emailValidator = new Validator<string>()
.addRule(value => value.length === 0 ? 'Email is required' : null)
.addRule(value => !value.includes('@') ? 'Invalid email format' : null);

const passwordValidator = new Validator<string>()
.addRule(value => value.length < 8 ? 'Password must be at least 8 characters' : null)
.addRule(value => !/[A-Z]/.test(value) ? 'Password must contain uppercase letter' : null)
.addRule(value => !/[0-9]/.test(value) ? 'Password must contain a number' : null);

// Использование
const emailErrors = emailValidator.validate('invalid-email');
const isValidPassword = passwordValidator.isValid('MyPassword123');
Производительность

Используйте строгую типизацию TypeScript для предотвращения ошибок на этапе компиляции и улучшения производительности IDE.

Типобезопасность

Избегайте использования any типа. Предпочитайте unknown для неизвестных типов и используйте type guards для проверки типов.

Паттерны проектирования

Builder Pattern

interface User {
id: number;
name: string;
email: string;
age?: number;
}

class UserBuilder {
private user: Partial<User> = {};

id(id: number): this {
this.user.id = id;
return this;
}

name(name: string): this {
this.user.name = name;
return this;
}

email(email: string): this {
this.user.email = email;
return this;
}

age(age: number): this {
this.user.age = age;
return this;
}

build(): User {
if (!this.user.id || !this.user.name || !this.user.email) {
throw new Error('Missing required fields');
}
return this.user as User;
}
}

// Использование
const user = new UserBuilder()
.id(1)
.name('John Doe')
.email('john@example.com')
.age(30)
.build();

Repository Pattern

interface Repository<T, K> {
findById(id: K): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: K, entity: Partial<T>): Promise<T>;
delete(id: K): Promise<boolean>;
}

class UserRepository implements Repository<User, number> {
private api: ApiClient;

constructor(api: ApiClient) {
this.api = api;
}

async findById(id: number): Promise<User | null> {
const response = await this.api.get<User>(`/users/${id}`);
return response.success ? response.data : null;
}

async findAll(): Promise<User[]> {
const response = await this.api.get<User[]>('/users');
return response.success ? response.data : [];
}

async create(userData: Omit<User, 'id'>): Promise<User> {
const response = await this.api.post<User>('/users', userData);
if (!response.success) {
throw new Error('Failed to create user');
}
return response.data;
}

async update(id: number, userData: Partial<User>): Promise<User> {
const response = await this.api.put<User>(`/users/${id}`, userData);
if (!response.success) {
throw new Error('Failed to update user');
}
return response.data;
}

async delete(id: number): Promise<boolean> {
const response = await this.api.delete(`/users/${id}`);
return response.success;
}
}

Event System

Типизированный Event Emitter

type EventCallback<T = any> = (data: T) => void;

class EventEmitter<T extends Record<string, any> = Record<string, any>> {
private events: { [K in keyof T]?: EventCallback<T[K]>[] } = {};

on<K extends keyof T>(event: K, callback: EventCallback<T[K]>): void {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event]!.push(callback);
}

off<K extends keyof T>(event: K, callback: EventCallback<T[K]>): void {
if (!this.events[event]) return;

const index = this.events[event]!.indexOf(callback);
if (index > -1) {
this.events[event]!.splice(index, 1);
}
}

emit<K extends keyof T>(event: K, data: T[K]): void {
if (!this.events[event]) return;

this.events[event]!.forEach(callback => callback(data));
}

once<K extends keyof T>(event: K, callback: EventCallback<T[K]>): void {
const onceCallback: EventCallback<T[K]> = (data) => {
callback(data);
this.off(event, onceCallback);
};
this.on(event, onceCallback);
}
}

// Типизированные события
interface AppEvents {
userLogin: { userId: number; name: string };
userLogout: { userId: number };
error: { message: string; code: number };
}

const emitter = new EventEmitter<AppEvents>();

emitter.on('userLogin', (data) => {
console.log(`User ${data.name} logged in`);
});

emitter.emit('userLogin', { userId: 1, name: 'John' });

Кеширование и мемоизация

Cache класс

class Cache<K, V> {
private cache = new Map<K, V>();
private maxSize: number;
private ttl: number;
private timers = new Map<K, NodeJS.Timeout>();

constructor(maxSize = 100, ttl = 300000) { // 5 минут по умолчанию
this.maxSize = maxSize;
this.ttl = ttl;
}

set(key: K, value: V): void {
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
const firstKey = this.cache.keys().next().value;
this.delete(firstKey);
}

this.cache.set(key, value);

if (this.timers.has(key)) {
clearTimeout(this.timers.get(key)!);
}

const timer = setTimeout(() => {
this.delete(key);
}, this.ttl);

this.timers.set(key, timer);
}

get(key: K): V | undefined {
return this.cache.get(key);
}

has(key: K): boolean {
return this.cache.has(key);
}

delete(key: K): boolean {
const timer = this.timers.get(key);
if (timer) {
clearTimeout(timer);
this.timers.delete(key);
}
return this.cache.delete(key);
}

clear(): void {
this.timers.forEach(timer => clearTimeout(timer));
this.timers.clear();
this.cache.clear();
}

size(): number {
return this.cache.size;
}
}

// Мемоизация
function memoize<T extends (...args: any[]) => any>(
fn: T,
cache = new Cache<string, ReturnType<T>>()
): T {
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key)!;
}

const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}

// Использование
const expensiveFunction = memoize((n: number): number => {
console.log(`Computing for ${n}`);
return n * n;
});

console.log(expensiveFunction(5)); // Computing for 5, returns 25
console.log(expensiveFunction(5)); // returns 25 (from cache)

Утилиты

Debounce и Throttle

function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
immediate = false
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;

return function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null;
if (!immediate) func(...args);
};

const callNow = immediate && !timeout;

if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);

if (callNow) func(...args);
};
}

function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;

return function executedFunction(...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}

// Использование
const debouncedSearch = debounce((query: string) => {
console.log('Searching for:', query);
}, 300);

const throttledResize = throttle(() => {
console.log('Window resized');
}, 100);

Продвинутые типы

// Условные типы
type NonNullable<T> = T extends null | undefined ? never : T;

// Извлечение типа из массива
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;

// Глубокое частичное
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Исключение ключей по типу
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};

// Извлечение функций
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

// Промисы
type PromiseReturnType<T> = T extends Promise<infer R> ? R : T;

// Tuple к Union
type TupleToUnion<T extends ReadonlyArray<any>> = T[number];