SEPEHR.SYS

Starting SEPEHR.SYS…

Loading

سپهر محسنی

مهندس نرم‌افزار فول‌استک و هوش مصنوعی

NOTES.TXT ×

الگوهای پیشرفتهٔ تایپ‌اسکریپت: کدی Type-Safe که مقیاس‌پذیر می‌ماند

7′ · ۱۶ دی ۱۴۰۴

سیستم تایپ تایپ‌اسکریپت به‌طرز شگفت‌انگیزی قدرتمند است. در این راهنما الگوهای پیشرفته‌ای را بررسی می‌کنیم که به شما کمک می‌کنند کدی امن‌تر و قابل‌نگهداری‌تر بنویسید و هم‌زمان از تمام ظرفیت تایپ‌اسکریپت بهره بگیرید.

محدودیت‌های جنریک و استنتاج تایپ

جنریک‌های پایه با محدودیت (Constraint)

// Constrain generic to objects with an id property
interface HasId {
  id: string | number;
}

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

// Usage
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'John', email: 'john@example.com' },
  { id: 2, name: 'Jane', email: 'jane@example.com' },
];

const user = findById(users, 1); // Type: User | undefined

توابع Factory جنریک

// Factory function with proper type inference
function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<(state: T) => void>();

  return {
    getState: () => state,
    setState: (newState: Partial<T>) => {
      state = { ...state, ...newState };
      listeners.forEach(listener => listener(state));
    },
    subscribe: (listener: (state: T) => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

// Type is inferred from initial state
const userStore = createStore({
  user: null as User | null,
  isLoading: false,
  error: null as string | null,
});

userStore.setState({ isLoading: true }); // Type-safe!

<Callout type="info"> تایپ‌اسکریپت می‌تواند تایپ‌های پیچیده را از روی نحوهٔ استفاده استنتاج کند. هر جا که ممکن است بگذارید کامپایلر این کار را انجام دهد، اما برای APIهای عمومی تایپ‌ها را به‌صورت صریح بنویسید. </Callout>

تایپ‌های شرطی (Conditional Types)

تایپ‌های شرطی پایه

// Extract return type based on input
type ApiResponse<T> = T extends 'user' 
  ? User 
  : T extends 'product' 
    ? Product 
    : never;

// Usage
type UserResponse = ApiResponse<'user'>; // User
type ProductResponse = ApiResponse<'product'>; // Product

// Practical example: Unwrap Promise types
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type Result = Awaited<Promise<Promise<string>>>; // string

تایپ‌های شرطی توزیع‌شونده (Distributive)

// Conditional types distribute over unions
type NonNullable<T> = T extends null | undefined ? never : T;

type Example = NonNullable<string | null | undefined>; // string

// Extract specific types from union
type ExtractStrings<T> = T extends string ? T : never;

type Mixed = string | number | boolean | 'hello' | 'world';
type OnlyStrings = ExtractStrings<Mixed>; // string | 'hello' | 'world'

کلیدواژهٔ infer برای استخراج تایپ

// Extract function parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;

// Practical example: Extract props from React component
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

// Usage
function fetchUser(id: number): Promise<User> { /* ... */ }

type FetchUserParams = Parameters<typeof fetchUser>; // [number]
type FetchUserReturn = ReturnType<typeof fetchUser>; // Promise<User>

تایپ‌های نگاشت‌شده (Mapped Types)

تبدیل تایپ‌های شیء

// Make all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Make all properties required
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Pick specific properties
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

تایپ‌های نگاشت‌شدهٔ پیشرفته

// Create getters for all properties
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Filter properties by type
type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  count: number;
}

type NumberProps = FilterByType<Mixed, number>;
// { id: number; count: number; }

<Note title="نکتهٔ تایپ"> از تایپ‌های Template Literal در ترکیب با Mapped Types استفاده کنید تا تبدیل‌های تایپی قدرتمندی برای کلاینت‌های API، فرم‌سازها و موارد دیگر بسازید. </Note>

تایپ‌گاردها (Type Guards) و باریک‌سازی تایپ

تایپ‌گاردهای سفارشی

// Type predicate function
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'email' in value &&
    typeof (value as User).id === 'number' &&
    typeof (value as User).email === 'string'
  );
}

// Usage
function processData(data: unknown) {
  if (isUser(data)) {
    // TypeScript knows data is User here
    console.log(data.email);
  }
}

// Discriminated unions with type guards
interface SuccessResponse {
  status: 'success';
  data: User;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    // TypeScript narrows to SuccessResponse
    console.log(response.data.name);
  } else {
    // TypeScript narrows to ErrorResponse
    console.log(response.message);
  }
}

توابع Assertion

// Assert function that throws if condition is false
function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error('Value is not a User');
  }
}

// Usage
function processUserData(data: unknown) {
  assertIsUser(data);
  // After assertion, TypeScript knows data is User
  console.log(data.email);
}

// Assert non-null
function assertDefined<T>(
  value: T | null | undefined,
  message?: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(message ?? 'Value is not defined');
  }
}

تایپ‌های Template Literal

ساخت APIهای Type-Safe

// Event handler types
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'

// Route parameters
type Route = '/users/:id' | '/posts/:postId/comments/:commentId';

type ExtractParams<T extends string> = 
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : T extends `${infer _Start}:${infer Param}`
      ? Param
      : never;

type UserRouteParams = ExtractParams<'/users/:id'>; // 'id'
type CommentRouteParams = ExtractParams<'/posts/:postId/comments/:commentId'>;
// 'postId' | 'commentId'

پراپرتی‌های CSS به‌صورت Type-Safe

type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

interface Spacing {
  margin: CSSValue;
  padding: CSSValue;
}

const spacing: Spacing = {
  margin: '16px',    // Valid
  padding: '1.5rem', // Valid
  // margin: '16',   // Error: not a valid CSSValue
};

تایپ‌های کمکی (Utility Types) در عمل

ساخت یک کتابخانهٔ فرم

// Form field configuration
type FieldConfig<T> = {
  [K in keyof T]: {
    type: T[K] extends string 
      ? 'text' | 'email' | 'password'
      : T[K] extends number 
        ? 'number'
        : T[K] extends boolean 
          ? 'checkbox'
          : 'text';
    label: string;
    required?: boolean;
    validate?: (value: T[K]) => string | undefined;
  };
};

interface UserForm {
  name: string;
  email: string;
  age: number;
  newsletter: boolean;
}

const userFormConfig: FieldConfig<UserForm> = {
  name: {
    type: 'text',
    label: 'Full Name',
    required: true,
    validate: (value) => value.length < 2 ? 'Name too short' : undefined,
  },
  email: {
    type: 'email',
    label: 'Email Address',
    required: true,
  },
  age: {
    type: 'number',
    label: 'Age',
  },
  newsletter: {
    type: 'checkbox',
    label: 'Subscribe to newsletter',
  },
};

کلاینت Type-Safe برای API

// Define API endpoints
interface ApiEndpoints {
  '/users': {
    GET: { response: User[] };
    POST: { body: CreateUserDTO; response: User };
  };
  '/users/:id': {
    GET: { response: User };
    PUT: { body: UpdateUserDTO; response: User };
    DELETE: { response: void };
  };
}

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

// Extract response type for endpoint and method
type ApiResponse<
  E extends keyof ApiEndpoints,
  M extends keyof ApiEndpoints[E]
> = ApiEndpoints[E][M] extends { response: infer R } ? R : never;

// Type-safe fetch wrapper
async function apiClient<
  E extends keyof ApiEndpoints,
  M extends keyof ApiEndpoints[E] & HttpMethod
>(
  endpoint: E,
  method: M,
  options?: ApiEndpoints[E][M] extends { body: infer B } ? { body: B } : never
): Promise<ApiResponse<E, M>> {
  const response = await fetch(endpoint as string, {
    method,
    body: options?.body ? JSON.stringify(options.body) : undefined,
  });
  return response.json();
}

// Usage - fully type-safe!
const users = await apiClient('/users', 'GET');
// Type: User[]

const newUser = await apiClient('/users', 'POST', {
  body: { name: 'John', email: 'john@example.com' },
});
// Type: User

جمع‌بندی

الگوهای پیشرفتهٔ تایپ‌اسکریپت به شما امکان می‌دهند اپلیکیشن‌هایی Type-Safe بسازید که خطاها را به‌جای زمان اجرا (runtime)، در زمان کامپایل شکار می‌کنند. با تسلط بر جنریک‌ها، تایپ‌های شرطی، Mapped Types و تایپ‌گاردها می‌توانید APIهایی بسازید که هم انعطاف‌پذیرند و هم ضدضربه.

نکات کلیدی:

  • برای توابع قابل‌استفادهٔ مجدد و Type-Safe از جنریک‌های دارای محدودیت استفاده کنید
  • برای تبدیل‌های تایپی پیچیده از تایپ‌های شرطی بهره بگیرید
  • برای اعمال تغییرات نظام‌مند روی تایپ‌ها، Mapped Type تعریف کنید
  • برای بررسی تایپ در زمان اجرا، تایپ‌گارد پیاده‌سازی کنید
  • این الگوها را با هم ترکیب کنید تا به APIهایی قدرتمند و Type-Safe برسید

این یادداشت ترجمهٔ فارسی نوشتهٔ خودم است — نسخهٔ اصلی (انگلیسی) در dev.to