TypeScript چیست؟ آموزش کامل TypeScript با ساخت SDK برای API هوش مصنوعی

در این آموزش TypeScript را از مفاهیم پایه تا Generic، Narrowing، Utility Type و تنظیمات Strict یاد می‌گیرید. سپس یک SDK تایپ‌شده، قابل تست و کاربردی برای API هوش مصنوعی درواره می‌سازیم.

Share
TypeScript چیست؟ آموزش کامل TypeScript با ساخت SDK برای API هوش مصنوعی


TypeScript یکی از مهم‌ترین فناوری‌های توسعه نرم‌افزار مدرن است. بسیاری از پروژه‌های Frontend، Backend، Full Stack، ابزارهای خط فرمان، Extensionها و SDKهای جدید با TypeScript توسعه داده می‌شوند.

TypeScript امکانات JavaScript را حفظ می‌کند و یک Type System ایستا به آن اضافه می‌کند. این سیستم می‌تواند بسیاری از خطاهای مربوط به نوع داده، Propertyهای اشتباه، پارامترهای ناقص و خروجی‌های پیش‌بینی‌نشده را پیش از اجرای برنامه شناسایی کند.

در این آموزش، TypeScript را فقط در حد تعریف چند Type ساده یاد نمی‌گیریم. ابتدا مفاهیم مهمی مانند Type Inference، Interface، Union، Literal Type، Narrowing، Generic، Utility Type، unknown، never و تنظیمات Strict را بررسی می‌کنیم. سپس یک SDK واقعی برای API هوش مصنوعی درواره می‌سازیم که ویژگی‌های زیر را دارد:

  • Typeهای مشخص برای Request و Response
  • مدیریت خطای ساخت‌یافته
  • Timeout
  • پشتیبانی از AbortSignal
  • اعتبارسنجی Runtime پاسخ API
  • قابلیت جایگزینی fetch برای تست
  • خروجی ESM
  • فایل‌های Declaration
  • Unit Test بدون ارسال درخواست واقعی
  • نگهداری امن‌تر API Key در متغیر محیطی

TypeScript چیست؟

TypeScript یک زبان برنامه‌نویسی مبتنی بر JavaScript است که Static Type Checking را به آن اضافه می‌کند.

کد TypeScript معمولاً قبل از اجرا به JavaScript تبدیل می‌شود. مرورگر یا Node.js در نهایت کد JavaScript تولیدشده را اجرا می‌کند.

مثال JavaScript:

function add(a, b) {
  return a + b;
}

مثال TypeScript:

function add(a: number, b: number): number {
  return a + b;
}

در نسخه TypeScript مشخص کرده‌ایم:

  • a باید number باشد.
  • b باید number باشد.
  • خروجی Function نیز number است.

اگر بنویسیم:

add("10", 20);

TypeScript پیش از اجرای برنامه خطا می‌دهد؛ زیرا مقدار "10" از نوع string است، نه number.

طبق مستندات پایه TypeScript، Type System ایستا تلاش می‌کند قبل از Runtime درباره رفتار احتمالی کد و ناسازگاری Typeها هشدار دهد.

تفاوت TypeScript و JavaScript

JavaScript یک زبان Dynamically Typed است. Type متغیرها در Runtime مشخص می‌شود و یک متغیر می‌تواند در طول اجرای برنامه مقادیر متفاوتی بگیرد:

let value = 10;
value = "hello";
value = true;

TypeScript می‌تواند Type متغیر را محدود کند:

let value: number = 10;

value = "hello";

خطای TypeScript:

Type 'string' is not assignable to type 'number'.

تفاوت‌های اصلی:

Type Checking

JavaScript بیشتر خطاهای Type را هنگام اجرا آشکار می‌کند. TypeScript می‌تواند تعداد زیادی از این خطاها را هنگام توسعه یا Build شناسایی کند.

تکمیل خودکار بهتر

وقتی Type داده مشخص باشد، ویرایشگر می‌تواند Propertyها، Methodها و پارامترهای معتبر را دقیق‌تر پیشنهاد دهد.

Refactoring مطمئن‌تر

تغییر نام Function یا Property در پروژه بزرگ با Type Information قابل‌ردیابی‌تر است.

مستندسازی داخل کد

Typeها مشخص می‌کنند هر Function چه ورودی و خروجی دارد.

نیاز به Build

کد TypeScript معمولاً باید به JavaScript تبدیل شود، مگر اینکه Runtime مورد استفاده پشتیبانی خاصی برای اجرای مستقیم TypeScript ارائه دهد.

آیا TypeScript جایگزین JavaScript است؟

TypeScript روی JavaScript ساخته شده است. بیشتر سینتکس JavaScript در TypeScript معتبر است و TypeScript نیز در نهایت معمولاً به JavaScript تبدیل می‌شود.

برای یادگیری حرفه‌ای TypeScript همچنان باید JavaScript را بشناسید:

  • Variable
  • Function
  • Object
  • Array
  • Promise
  • Module
  • Class
  • Closure
  • Event Loop
  • async و await
  • Error Handling

TypeScript ضعف درک JavaScript را به‌طور کامل جبران نمی‌کند؛ بلکه لایه‌ای برای تحلیل Type و بهبود Tooling فراهم می‌کند.

TypeScript چه مشکلاتی را حل می‌کند؟

در پروژه‌های کوچک JavaScript ممکن است Typeهای داده را به‌سادگی در ذهن نگه دارید. با بزرگ‌ترشدن پروژه، تعداد Functionها، APIها و Objectها افزایش پیدا می‌کند.

یک خطای رایج:

function printUser(user) {
  console.log(user.fullName.toUpperCase());
}

اگر API به‌جای fullName مقدار name برگرداند، برنامه در Runtime خطا می‌دهد:

Cannot read properties of undefined

در TypeScript:

interface User {
  fullName: string;
}

function printUser(user: User): void {
  console.log(user.fullName.toUpperCase());
}

اگر Object ورودی fullName نداشته باشد، TypeScript هنگام توسعه هشدار می‌دهد.

آیا TypeScript تمام خطاها را حذف می‌کند؟

خیر. TypeScript می‌تواند خطاهای زیادی را پیش از اجرا پیدا کند، اما این موارد را تضمین نمی‌کند:

  • صحت منطق تجاری
  • درستی پاسخ API خارجی
  • وجود واقعی فایل یا Network
  • معتبر بودن داده پایگاه داده
  • موفق‌بودن Request
  • نبودن Race Condition
  • عملکرد صحیح UI
  • معتبر بودن ورودی کاربر
  • نبودن خطای Runtime در Library
  • صحت Type Assertionهای توسعه‌دهنده

Typeهای TypeScript هنگام اجرای JavaScript معمولاً وجود ندارند. بنابراین داده‌ای که از API، فایل، کاربر یا پایگاه داده دریافت می‌شود باید در Runtime نیز اعتبارسنجی شود.

این موضوع در ساخت SDK اهمیت زیادی دارد.

نصب TypeScript

برای شروع به Node.js و npm نیاز دارید.

بررسی نسخه:

node --version
npm --version

ساخت پروژه:

mkdir darvareh-typescript-sdk
cd darvareh-typescript-sdk
npm init -y

نصب TypeScript:

npm install --save-dev typescript

بررسی نسخه Compiler:

npx tsc --version

ساخت فایل تنظیمات:

npx tsc --init

فرمان tsc مخفف TypeScript Compiler است.

اولین برنامه TypeScript

فایل hello.ts:

function greet(name: string): string {
  return `سلام ${name}`;
}

const message = greet("درواره");

console.log(message);

Type Check:

npx tsc hello.ts --noEmit

تبدیل به JavaScript:

npx tsc hello.ts

فایل hello.js ساخته می‌شود.

اجرا:

node hello.js

Type Annotation چیست؟

Type Annotation نوع یک Variable، Parameter یا خروجی را به‌صورت صریح مشخص می‌کند:

const modelName: string = "example-model";
const maxTokens: number = 1000;
const streaming: boolean = false;

Function:

function calculateCost(
  inputTokens: number,
  outputTokens: number,
): number {
  return inputTokens + outputTokens;
}

همیشه لازم نیست Type را صریح بنویسید. TypeScript در بسیاری از موارد Type را Infer می‌کند.

Type Inference چیست؟

TypeScript می‌تواند Type را از مقدار اولیه تشخیص دهد:

const modelName = "example-model";

Type متغیر به‌صورت خودکار string تشخیص داده می‌شود.

const maxTokens = 1000;

Type آن number است.

این کد معمولاً بیش از حد توضیحی است:

const name: string = "Darvareh";

این نسخه نیز Type Safe است:

const name = "Darvareh";

Type Annotation بیشتر زمانی مفید است که:

  • Type از مقدار اولیه روشن نیست.
  • API عمومی طراحی می‌کنید.
  • Function خروجی پیچیده دارد.
  • Contract میان بخش‌ها باید صریح باشد.
  • می‌خواهید Type گسترده‌تر یا محدودتری تعریف کنید.

Typeهای پایه TypeScript

string

const prompt: string = "یک متن خلاصه بنویس";

number

const temperature: number = 0.3;

TypeScript برای عدد صحیح و اعشاری Type جداگانه‌ای ندارد و هر دو number هستند.

boolean

const stream: boolean = true;

bigint

const largeValue: bigint = 9007199254740993n;

symbol

const requestId: symbol = Symbol("request-id");

null و undefined

const emptyValue: null = null;
const missingValue: undefined = undefined;

با فعال‌بودن strictNullChecks، null و undefined باید به‌صورت مشخص مدیریت شوند.

Array در TypeScript

دو روش رایج:

const models: string[] = [
  "model-a",
  "model-b",
];

یا:

const models: Array<string> = [
  "model-a",
  "model-b",
];

Array از Object:

interface Model {
  id: string;
  title: string;
}

const models: Model[] = [
  {
    id: "model-a",
    title: "Model A",
  },
];

Tuple چیست؟

Tuple یک Array با طول و Typeهای مشخص در موقعیت‌های مشخص است:

const usage: [number, number] = [
  1200,
  450,
];

نام‌گذاری اعضای Tuple خوانایی را بهتر می‌کند:

type TokenUsage = [
  inputTokens: number,
  outputTokens: number,
];

const usage: TokenUsage = [1200, 450];

برای داده‌های پیچیده یا قابل توسعه، Object معمولاً از Tuple خواناتر است:

interface TokenUsage {
  inputTokens: number;
  outputTokens: number;
}

Object Type

تعریف مستقیم:

const model: {
  id: string;
  contextWindow: number;
  supportsVision: boolean;
} = {
  id: "model-a",
  contextWindow: 128000,
  supportsVision: true,
};

برای استفاده مجدد بهتر است Type یا Interface بسازید.

Interface در TypeScript

interface AIModel {
  id: string;
  contextWindow: number;
  supportsVision: boolean;
}

استفاده:

const model: AIModel = {
  id: "model-a",
  contextWindow: 128000,
  supportsVision: true,
};

Property اختیاری:

interface AIModel {
  id: string;
  contextWindow: number;
  description?: string;
}

Property فقط‌خواندنی:

interface AIModel {
  readonly id: string;
  contextWindow: number;
}

بعد از ساخت Object نمی‌توان id را از طریق TypeScript تغییر داد:

model.id = "new-model";

اما readonly به‌تنهایی Immutableبودن عمیق Object در Runtime را تضمین نمی‌کند.

Type Alias

type ModelId = string;

Object Type:

type AIModel = {
  id: string;
  contextWindow: number;
};

Union:

type ResponseStyle =
  | "short"
  | "detailed"
  | "bullet";

Function Type:

type Logger = (
  message: string,
  metadata?: Record<string, unknown>,
) => void;

تفاوت type و interface

هر دو می‌توانند شکل Object را تعریف کنند.

Interface:

interface ChatRequest {
  model: string;
  messages: ChatMessage[];
}

Type:

type ChatRequest = {
  model: string;
  messages: ChatMessage[];
};

تفاوت‌های مهم:

  • interface قابلیت Declaration Merging دارد.
  • type برای Union، Tuple، Conditional Type و ترکیب‌های پیشرفته انعطاف بیشتری دارد.
  • هر دو قابلیت Extension یا Composition دارند.
  • برای بیشتر Objectهای معمول، هر دو قابل استفاده‌اند.

Extension در Interface:

interface BaseResponse {
  id: string;
}

interface ChatResponse extends BaseResponse {
  choices: ChatChoice[];
}

Intersection در Type:

type ChatResponse =
  BaseResponse & {
    choices: ChatChoice[];
  };

بهتر است تیم یک Convention مشخص داشته باشد و Consistency را حفظ کند.

Union Type

Union یعنی مقدار می‌تواند یکی از چند Type باشد:

type ModelId = string | number;

Literal Union:

type Role =
  | "system"
  | "user"
  | "assistant"
  | "tool";

استفاده:

interface ChatMessage {
  role: Role;
  content: string;
}

مقدار نامعتبر هنگام توسعه رد می‌شود:

const message: ChatMessage = {
  role: "customer",
  content: "Hello",
};

Literal Type

وقتی با const مقدار ثابتی تعریف می‌کنید، TypeScript می‌تواند Type محدودتری Infer کند:

const role = "user";

Type این مقدار ممکن است Literal "user" باشد، نه string عمومی.

Objectها به‌طور پیش‌فرض ممکن است Type گسترده‌تری بگیرند:

const message = {
  role: "user",
  content: "Hello",
};

برای حفظ Literal Type:

const message = {
  role: "user",
  content: "Hello",
} as const;

as const Propertyها را نیز Readonly می‌کند و Literal Typeها را حفظ می‌کند.

Intersection Type

Intersection چند Type را ترکیب می‌کند:

type Identifiable = {
  id: string;
};

type Timestamped = {
  createdAt: string;
};

type StoredMessage =
  Identifiable &
  Timestamped & {
    role: "user" | "assistant";
    content: string;
  };

Object نهایی باید تمام Propertyهای Typeهای ترکیب‌شده را داشته باشد.

Optional Property

interface ChatRequest {
  model: string;
  temperature?: number;
}

علامت ? یعنی Property ممکن است وجود نداشته باشد.

این دو حالت از نظر رفتار Runtime همیشه یکسان نیستند:

{}

و:

{
  temperature: undefined,
}

تنظیم exactOptionalPropertyTypes تفاوت میان «نبودن Property» و «وجود Property با مقدار undefined» را دقیق‌تر بررسی می‌کند. جزئیات این رفتار در مرجع TSConfig توضیح داده شده است.

Function در TypeScript

function createPrompt(
  topic: string,
  length: number,
): string {
  return `درباره ${topic} در ${length} کلمه بنویس.`;
}

Arrow Function:

const createPrompt = (
  topic: string,
  length: number,
): string => {
  return `درباره ${topic} در ${length} کلمه بنویس.`;
};

پارامتر اختیاری:

function greet(
  name: string,
  title?: string,
): string {
  return title
    ? `سلام ${title} ${name}`
    : `سلام ${name}`;
}

مقدار پیش‌فرض:

function createRequest(
  prompt: string,
  temperature = 0.3,
): object {
  return {
    prompt,
    temperature,
  };
}

Type خروجی void:

function logMessage(message: string): void {
  console.log(message);
}

unknown در برابر any

any

any بررسی Type را عملاً غیرفعال می‌کند:

let value: any;

value.toUpperCase();
value.notExistingMethod();
value.foo.bar();

TypeScript روی این عملیات‌ها هشدار مؤثری نمی‌دهد.

unknown

unknown یعنی Type مقدار هنوز مشخص نیست:

let value: unknown;

قبل از استفاده باید آن را Narrow کنید:

if (typeof value === "string") {
  console.log(value.toUpperCase());
}

برای پاسخ response.json()، داده فایل یا ورودی خارجی، unknown معمولاً نقطه شروع ایمن‌تری از any است.

Type Narrowing چیست؟

Narrowing یعنی TypeScript با بررسی مسیر اجرای برنامه، Type گسترده را به Type مشخص‌تری تبدیل کند.

function normalizeId(
  id: string | number,
): string {
  if (typeof id === "number") {
    return id.toString();
  }

  return id.trim();
}

داخل شرط، Type مقدار number است. بعد از شرط، TypeScript می‌داند که مقدار باقی‌مانده string است.

طبق مستندات Narrowing در TypeScript، TypeScript از Type Guard و Control Flow Analysis برای محدودکردن Type استفاده می‌کند.

روش‌های رایج Narrowing:

  • typeof
  • instanceof
  • in
  • Array.isArray
  • مقایسه با Literal
  • Truthiness Check
  • Type Predicate
  • Discriminated Union

Type Guard سفارشی

function isStringArray(
  value: unknown,
): value is string[] {
  return (
    Array.isArray(value) &&
    value.every(
      (item) => typeof item === "string",
    )
  );
}

استفاده:

const value: unknown = [
  "model-a",
  "model-b",
];

if (isStringArray(value)) {
  value.map((item) => item.toUpperCase());
}

عبارت value is string[] یک Type Predicate است.

Discriminated Union

برای مدل‌کردن چند وضعیت مرتبط:

type RequestState =
  | {
      status: "idle";
    }
  | {
      status: "loading";
    }
  | {
      status: "success";
      data: string;
    }
  | {
      status: "error";
      message: string;
    };

استفاده:

function renderState(
  state: RequestState,
): string {
  switch (state.status) {
    case "idle":
      return "آماده";

    case "loading":
      return "در حال دریافت";

    case "success":
      return state.data;

    case "error":
      return state.message;
  }
}

Property مشترک status نقش Discriminant را دارد و TypeScript بر اساس آن Type هر Branch را تشخیص می‌دهد.

never و بررسی Exhaustive

never وضعیتی را نشان می‌دهد که نباید مقدار معتبری داشته باشد.

function assertNever(value: never): never {
  throw new Error(
    `Unexpected value: ${String(value)}`,
  );
}

استفاده در Switch:

function renderState(
  state: RequestState,
): string {
  switch (state.status) {
    case "idle":
      return "آماده";

    case "loading":
      return "در حال دریافت";

    case "success":
      return state.data;

    case "error":
      return state.message;

    default:
      return assertNever(state);
  }
}

اگر بعداً وضعیت جدیدی به Union اضافه شود اما در Switch مدیریت نشود، TypeScript می‌تواند خطا را آشکار کند.

Generic چیست؟

Generic امکان ساخت Component، Function یا Type قابل استفاده برای داده‌های مختلف را فراهم می‌کند، بدون آنکه ارتباط Type ورودی و خروجی از بین برود.

Function ساده:

function identity<T>(value: T): T {
  return value;
}

استفاده:

const text = identity("hello");
const count = identity(10);

TypeScript معمولاً T را Infer می‌کند.

Generic برای پاسخ API:

interface APIResponse<T> {
  data: T;
  requestId: string;
}

پاسخ مدل:

interface Model {
  id: string;
  name: string;
}

type ModelsResponse = APIResponse<Model[]>;

طبق مستندات Generics در TypeScript، Generic Typeها ارتباط میان Typeهای ورودی، خروجی و اعضای ساختار را حفظ می‌کنند.

Generic Constraint

گاهی Generic باید Property مشخصی داشته باشد:

function getLength<T extends { length: number }>(
  value: T,
): number {
  return value.length;
}

این Function با string و Array کار می‌کند:

getLength("hello");
getLength([1, 2, 3]);

اما Object بدون length پذیرفته نمی‌شود.

keyof

keyof از کلیدهای یک Object Type یک Union می‌سازد:

interface Model {
  id: string;
  contextWindow: number;
  active: boolean;
}

type ModelKey = keyof Model;

نتیجه:

"id" | "contextWindow" | "active"

Generic همراه keyof:

function getProperty<
  T,
  K extends keyof T,
>(
  object: T,
  key: K,
): T[K] {
  return object[key];
}

استفاده:

const model = {
  id: "model-a",
  contextWindow: 128000,
};

const id = getProperty(model, "id");

کلید نامعتبر رد می‌شود:

getProperty(model, "price");

Utility Typeهای TypeScript

TypeScript چند Utility Type داخلی برای تبدیل Typeها ارائه می‌کند. فهرست آن‌ها در مستندات Utility Types قرار دارد.

Partial

تمام Propertyها را اختیاری می‌کند:

interface ClientOptions {
  baseURL: string;
  timeoutMs: number;
}

type ClientOptionsUpdate =
  Partial<ClientOptions>;

Required

تمام Propertyها را اجباری می‌کند:

type CompleteOptions =
  Required<ClientOptions>;

Readonly

Propertyها را Readonly می‌کند:

type ReadonlyOptions =
  Readonly<ClientOptions>;

Pick

بخشی از Propertyها را انتخاب می‌کند:

interface Model {
  id: string;
  name: string;
  contextWindow: number;
  description: string;
}

type ModelSummary = Pick<
  Model,
  "id" | "name"
>;

Omit

بعضی Propertyها را حذف می‌کند:

type PublicModel = Omit<
  Model,
  "description"
>;

Record

یک Object با Key و Value مشخص می‌سازد:

type HeadersMap =
  Record<string, string>;

Literal Key:

type RoleLabel = Record<
  "system" | "user" | "assistant",
  string
>;

Awaited

Type داخل Promise را استخراج می‌کند:

type Result =
  Awaited<Promise<string>>;

نتیجه string است.

Type Assertion

Type Assertion به TypeScript می‌گوید توسعه‌دهنده درباره Type مقدار اطلاعات بیشتری دارد:

const input =
  document.querySelector(
    "#message",
  ) as HTMLTextAreaElement;

Type Assertion داده را در Runtime تبدیل یا اعتبارسنجی نمی‌کند.

این کد خطرناک است:

const response =
  value as ChatCompletionResponse;

اگر value ساختار درستی نداشته باشد، TypeScript در Runtime از شما محافظت نمی‌کند.

برای داده خارجی ابتدا Validation انجام دهید و سپس Assertion محدود و مستند استفاده کنید.

Non-null Assertion

const element =
  document.querySelector("#app")!;

علامت ! می‌گوید مقدار null نیست. اگر Element وجود نداشته باشد، برنامه ممکن است در Runtime خطا بدهد.

روش صریح‌تر:

const element =
  document.querySelector("#app");

if (!element) {
  throw new Error(
    "Element #app was not found",
  );
}

Class در TypeScript

class ModelRegistry {
  private readonly models =
    new Map<string, string>();

  add(id: string, name: string): void {
    this.models.set(id, name);
  }

  get(id: string): string | undefined {
    return this.models.get(id);
  }
}

Access Modifierهای رایج:

  • public
  • private
  • protected
  • readonly

Private در TypeScript می‌تواند محدودیت Compile-time ایجاد کند. برای Private Field واقعی JavaScript می‌توان از # استفاده کرد:

class Counter {
  #value = 0;

  increment(): void {
    this.#value += 1;
  }
}

Promise و async/await

async function loadModels(): Promise<string[]> {
  const response = await fetch("/models");

  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}`,
    );
  }

  const data: unknown = await response.json();

  if (!Array.isArray(data)) {
    throw new Error("Invalid response");
  }

  return data.filter(
    (item): item is string =>
      typeof item === "string",
  );
}

Type خروجی Functionهای async یک Promise است.

تنظیم حرفه‌ای tsconfig

برای پروژه SDK از تنظیمات Strict استفاده می‌کنیم.

فایل tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "useUnknownInCatchVariables": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,

    "noEmit": true,
    "resolveJsonModule": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true
  },
  "include": [
    "src/**/*.ts",
    "examples/**/*.ts",
    "tests/**/*.ts"
  ]
}

تنظیم Build جدا:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "rootDir": "src",
    "outDir": "dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "noEmitOnError": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "examples",
    "tests",
    "dist"
  ]
}

این فایل را با نام زیر ذخیره کنید:

tsconfig.build.json

توضیح تنظیمات مهم

strict

مجموعه‌ای از بررسی‌های سخت‌گیرانه Type را فعال می‌کند.

noUncheckedIndexedAccess

دسترسی به Array یا Object با Index را به‌صورت احتمالی undefined در نظر می‌گیرد:

const items = ["a", "b"];
const first = items[0];

Type مقدار first می‌تواند string | undefined باشد.

exactOptionalPropertyTypes

Property اختیاری را دقیق‌تر مدیریت می‌کند.

useUnknownInCatchVariables

مقدار catch را به‌جای any به‌صورت unknown در نظر می‌گیرد.

declaration

فایل‌های .d.ts برای مصرف‌کنندگان SDK تولید می‌کند.

noEmitOnError

در صورت وجود Type Error، خروجی Build تولید نمی‌شود.

پروژه عملی: ساخت SDK درواره با TypeScript

ساختار پروژه:

darvareh-typescript-sdk/
├── src/
│   ├── client.ts
│   ├── errors.ts
│   ├── index.ts
│   ├── parsers.ts
│   └── types.ts
├── examples/
│   └── basic.ts
├── tests/
│   └── client.test.ts
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── .env.example
└── .gitignore

نصب وابستگی‌ها

npm install dotenv

وابستگی‌های توسعه:

npm install --save-dev \
  typescript \
  tsx \
  @types/node

تنظیم package.json

{
  "name": "darvareh-ai-sdk-example",
  "version": "1.0.0",
  "private": true,
  "type": "module",

  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",

  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },

  "scripts": {
    "check": "tsc -p tsconfig.json",
    "build": "tsc -p tsconfig.build.json",
    "dev": "tsx examples/basic.ts",
    "test": "tsx --test tests/*.test.ts"
  },

  "dependencies": {
    "dotenv": "^17.0.0"
  },

  "devDependencies": {
    "@types/node": "^24.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

شماره نسخه‌ها در طول زمان تغییر می‌کنند. پس از نصب و آزمایش، فایل Lock تولیدشده را نگه دارید تا نصب پروژه قابل تکرارتر باشد.

تعریف Typeهای SDK

فایل src/types.ts:

export type ChatRole =
  | "system"
  | "user"
  | "assistant"
  | "tool";

export interface ChatMessage {
  role: ChatRole;
  content: string;
}

export interface ChatCompletionRequest {
  model: string;
  messages: ChatMessage[];

  temperature?: number;
  max_tokens?: number;
  stream?: false;
}

export interface ChatChoice {
  index: number;

  message: {
    role: "assistant";
    content: string | null;
  };

  finish_reason: string | null;
}

export interface TokenUsage {
  prompt_tokens?: number;
  completion_tokens?: number;
  total_tokens?: number;
}

export interface ChatCompletionResponse {
  id: string;
  object?: string;
  created?: number;
  model: string;
  choices: ChatChoice[];
  usage?: TokenUsage;
}

export interface APIErrorBody {
  error?: {
    message?: string;
    type?: string;
    code?: string | number;
  };

  detail?: string;
}

export interface DarvarehClientOptions {
  apiKey: string;
  baseURL?: string;
  timeoutMs?: number;
  fetchImpl?: typeof fetch;
}

export interface RequestOptions {
  signal?: AbortSignal;
}

نکته مهم این است که نام بعضی فیلدهای API مانند max_tokens و finish_reason با Convention رایج JSON نوشته شده‌اند. در SDK می‌توانید یک Mapping به نام‌های Camel Case نیز طراحی کنید، اما در این نسخه Typeها را به Contract HTTP نزدیک نگه می‌داریم.

ساخت Errorهای اختصاصی

فایل src/errors.ts:

export class DarvarehSDKError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "DarvarehSDKError";
  }
}

export class DarvarehAPIError
  extends DarvarehSDKError {
  readonly status: number;
  readonly code?: string | number;
  readonly responseBody: unknown;

  constructor(options: {
    message: string;
    status: number;
    code?: string | number;
    responseBody: unknown;
  }) {
    super(options.message);

    this.name = "DarvarehAPIError";
    this.status = options.status;
    this.responseBody = options.responseBody;

    if (options.code !== undefined) {
      this.code = options.code;
    }
  }
}

export class DarvarehTimeoutError
  extends DarvarehSDKError {
  constructor(message = "Request timed out") {
    super(message);
    this.name = "DarvarehTimeoutError";
  }
}

export class DarvarehResponseError
  extends DarvarehSDKError {
  readonly responseBody: unknown;

  constructor(
    message: string,
    responseBody: unknown,
  ) {
    super(message);

    this.name = "DarvarehResponseError";
    this.responseBody = responseBody;
  }
}

با Errorهای اختصاصی، مصرف‌کننده SDK می‌تواند خطای HTTP، Timeout و پاسخ نامعتبر را از یکدیگر تشخیص دهد.

ساخت Parser پاسخ API

فایل src/parsers.ts:

import {
  DarvarehResponseError,
} from "./errors.js";

import type {
  APIErrorBody,
  ChatCompletionResponse,
} from "./types.js";


export function isRecord(
  value: unknown,
): value is Record<string, unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    !Array.isArray(value)
  );
}


export function parseAPIErrorBody(
  value: unknown,
): APIErrorBody {
  if (!isRecord(value)) {
    return {};
  }

  const result: APIErrorBody = {};

  if (
    typeof value.detail === "string"
  ) {
    result.detail = value.detail;
  }

  if (isRecord(value.error)) {
    const error: NonNullable<
      APIErrorBody["error"]
    > = {};

    if (
      typeof value.error.message === "string"
    ) {
      error.message = value.error.message;
    }

    if (
      typeof value.error.type === "string"
    ) {
      error.type = value.error.type;
    }

    if (
      typeof value.error.code === "string" ||
      typeof value.error.code === "number"
    ) {
      error.code = value.error.code;
    }

    result.error = error;
  }

  return result;
}


export function parseChatCompletionResponse(
  value: unknown,
): ChatCompletionResponse {
  if (!isRecord(value)) {
    throw new DarvarehResponseError(
      "Response must be an object",
      value,
    );
  }

  if (typeof value.id !== "string") {
    throw new DarvarehResponseError(
      "Response id is missing or invalid",
      value,
    );
  }

  if (typeof value.model !== "string") {
    throw new DarvarehResponseError(
      "Response model is missing or invalid",
      value,
    );
  }

  if (!Array.isArray(value.choices)) {
    throw new DarvarehResponseError(
      "Response choices must be an array",
      value,
    );
  }

  for (const choice of value.choices) {
    if (!isRecord(choice)) {
      throw new DarvarehResponseError(
        "A response choice is invalid",
        value,
      );
    }

    if (typeof choice.index !== "number") {
      throw new DarvarehResponseError(
        "Choice index is invalid",
        value,
      );
    }

    if (!isRecord(choice.message)) {
      throw new DarvarehResponseError(
        "Choice message is invalid",
        value,
      );
    }

    const content = choice.message.content;

    if (
      content !== null &&
      typeof content !== "string"
    ) {
      throw new DarvarehResponseError(
        "Message content is invalid",
        value,
      );
    }
  }

  return value as unknown as ChatCompletionResponse;
}

در انتهای Parser از Type Assertion استفاده شده است، اما تنها پس از بررسی فیلدهای اصلی پاسخ.

برای SDK عمومی و حساس‌تر می‌توانید تمام فیلدها را با Schema Validator یا Parser جامع‌تر بررسی کنید.

ساخت Client

فایل src/client.ts:

import {
  DarvarehAPIError,
  DarvarehSDKError,
  DarvarehTimeoutError,
} from "./errors.js";

import {
  parseAPIErrorBody,
  parseChatCompletionResponse,
} from "./parsers.js";

import type {
  ChatCompletionRequest,
  ChatCompletionResponse,
  DarvarehClientOptions,
  RequestOptions,
} from "./types.js";


const DEFAULT_BASE_URL =
  "https://api.darvareh.ir/v1";

const DEFAULT_TIMEOUT_MS = 60_000;


function normalizeBaseURL(
  baseURL: string,
): string {
  return baseURL.replace(/\/+$/, "");
}


async function readJSONSafely(
  response: Response,
): Promise<unknown> {
  const contentType =
    response.headers.get("content-type") ?? "";

  if (
    !contentType
      .toLowerCase()
      .includes("application/json")
  ) {
    const text = await response.text();

    return {
      raw: text,
    };
  }

  try {
    return await response.json();
  } catch {
    return null;
  }
}


export class DarvarehClient {
  readonly #apiKey: string;
  readonly #baseURL: string;
  readonly #timeoutMs: number;
  readonly #fetch: typeof fetch;

  constructor(
    options: DarvarehClientOptions,
  ) {
    const apiKey = options.apiKey.trim();

    if (!apiKey) {
      throw new DarvarehSDKError(
        "apiKey must not be empty",
      );
    }

    const timeoutMs =
      options.timeoutMs ??
      DEFAULT_TIMEOUT_MS;

    if (
      !Number.isFinite(timeoutMs) ||
      timeoutMs <= 0
    ) {
      throw new DarvarehSDKError(
        "timeoutMs must be a positive number",
      );
    }

    this.#apiKey = apiKey;
    this.#baseURL = normalizeBaseURL(
      options.baseURL ??
      DEFAULT_BASE_URL,
    );

    this.#timeoutMs = timeoutMs;
    this.#fetch = options.fetchImpl ?? fetch;
  }

  async createChatCompletion(
    request: ChatCompletionRequest,
    options: RequestOptions = {},
  ): Promise<ChatCompletionResponse> {
    this.#validateChatRequest(request);

    const responseBody =
      await this.#request(
        "/chat/completions",
        {
          method: "POST",
          headers: {
            "Authorization":
              `Bearer ${this.#apiKey}`,

            "Content-Type":
              "application/json",

            "Accept":
              "application/json",
          },
          body: JSON.stringify(request),
        },
        options,
      );

    return parseChatCompletionResponse(
      responseBody,
    );
  }

  async #request(
    path: string,
    init: RequestInit,
    options: RequestOptions,
  ): Promise<unknown> {
    const controller =
      new AbortController();

    const onExternalAbort = (): void => {
      controller.abort(
        options.signal?.reason,
      );
    };

    if (options.signal?.aborted) {
      onExternalAbort();
    } else {
      options.signal?.addEventListener(
        "abort",
        onExternalAbort,
        {
          once: true,
        },
      );
    }

    const timeout = setTimeout(
      () => {
        controller.abort(
          new Error("SDK_REQUEST_TIMEOUT"),
        );
      },
      this.#timeoutMs,
    );

    try {
      const response = await this.#fetch(
        `${this.#baseURL}${path}`,
        {
          ...init,
          signal: controller.signal,
        },
      );

      const body =
        await readJSONSafely(response);

      if (!response.ok) {
        const parsed =
          parseAPIErrorBody(body);

        const message =
          parsed.error?.message ??
          parsed.detail ??
          `API request failed with status ${response.status}`;

        const errorOptions: {
          message: string;
          status: number;
          code?: string | number;
          responseBody: unknown;
        } = {
          message,
          status: response.status,
          responseBody: body,
        };

        if (
          parsed.error?.code !== undefined
        ) {
          errorOptions.code =
            parsed.error.code;
        }

        throw new DarvarehAPIError(
          errorOptions,
        );
      }

      return body;
    } catch (error: unknown) {
      if (
        error instanceof DarvarehAPIError
      ) {
        throw error;
      }

      if (
        controller.signal.aborted &&
        !options.signal?.aborted
      ) {
        throw new DarvarehTimeoutError();
      }

      if (
        error instanceof Error &&
        error.name === "AbortError"
      ) {
        throw error;
      }

      if (error instanceof Error) {
        throw new DarvarehSDKError(
          `Network request failed: ${error.message}`,
        );
      }

      throw new DarvarehSDKError(
        "Network request failed",
      );
    } finally {
      clearTimeout(timeout);

      options.signal?.removeEventListener(
        "abort",
        onExternalAbort,
      );
    }
  }

  #validateChatRequest(
    request: ChatCompletionRequest,
  ): void {
    if (!request.model.trim()) {
      throw new DarvarehSDKError(
        "model must not be empty",
      );
    }

    if (request.messages.length === 0) {
      throw new DarvarehSDKError(
        "messages must not be empty",
      );
    }

    for (const message of request.messages) {
      if (!message.content.trim()) {
        throw new DarvarehSDKError(
          "message content must not be empty",
        );
      }
    }

    if (
      request.temperature !== undefined &&
      (
        request.temperature < 0 ||
        request.temperature > 2
      )
    ) {
      throw new DarvarehSDKError(
        "temperature must be between 0 and 2",
      );
    }

    if (
      request.max_tokens !== undefined &&
      (
        !Number.isInteger(
          request.max_tokens,
        ) ||
        request.max_tokens <= 0
      )
    ) {
      throw new DarvarehSDKError(
        "max_tokens must be a positive integer",
      );
    }
  }
}

ویژگی‌های تخصصی این Client:

  • از Private Field واقعی JavaScript استفاده می‌کند.
  • API Key را در Property عمومی قرار نمی‌دهد.
  • Base URL را Normalize می‌کند.
  • ورودی را قبل از Request بررسی می‌کند.
  • Timeout دارد.
  • AbortSignal خارجی را پشتیبانی می‌کند.
  • Errorهای HTTP را به Error اختصاصی تبدیل می‌کند.
  • پاسخ JSON را به‌عنوان unknown دریافت می‌کند.
  • پاسخ را در Runtime Parse می‌کند.
  • fetch قابل جایگزینی دارد و بنابراین تست آن ساده‌تر است.

ساخت فایل ورودی SDK

فایل src/index.ts:

export {
  DarvarehClient,
} from "./client.js";

export {
  DarvarehAPIError,
  DarvarehResponseError,
  DarvarehSDKError,
  DarvarehTimeoutError,
} from "./errors.js";

export type {
  APIErrorBody,
  ChatChoice,
  ChatCompletionRequest,
  ChatCompletionResponse,
  ChatMessage,
  ChatRole,
  DarvarehClientOptions,
  RequestOptions,
  TokenUsage,
} from "./types.js";

استفاده از export type کمک می‌کند Type Importها از Value Importها مشخص باشند.

فایل تنظیمات محیطی

فایل .env.example:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

فایل .gitignore:

.env
.env.*
!.env.example

node_modules/
dist/
coverage/

*.log
.DS_Store

فایل واقعی .env را بسازید و مقدارهای واقعی را داخل آن قرار دهید.

شناسه و قیمت مدل‌های موجود را در صفحه مدل‌های درواره بررسی کنید.

ساخت Example

فایل examples/basic.ts:

import "dotenv/config";

import {
  DarvarehAPIError,
  DarvarehClient,
  DarvarehSDKError,
  DarvarehTimeoutError,
} from "../src/index.js";


function getRequiredEnv(
  name: string,
): string {
  const value = process.env[name]?.trim();

  if (!value) {
    throw new Error(
      `Environment variable ${name} is required`,
    );
  }

  return value;
}


async function main(): Promise<void> {
  const client = new DarvarehClient({
    apiKey: getRequiredEnv(
      "DARVAREH_API_KEY",
    ),
    timeoutMs: 60_000,
  });

  const response =
    await client.createChatCompletion({
      model: getRequiredEnv(
        "DARVAREH_MODEL_ID",
      ),

      messages: [
        {
          role: "system",
          content: (
            "You are a helpful Persian assistant. " +
            "Answer clearly and accurately in Persian."
          ),
        },
        {
          role: "user",
          content: (
            "Generic در TypeScript را " +
            "با یک مثال ساده توضیح بده."
          ),
        },
      ],

      temperature: 0.3,
      max_tokens: 800,
      stream: false,
    });

  const answer =
    response.choices[0]?.message.content;

  if (!answer) {
    throw new Error(
      "The model returned an empty answer",
    );
  }

  console.log(answer);

  if (response.usage) {
    console.log(
      "Token usage:",
      response.usage,
    );
  }
}


main().catch((error: unknown) => {
  if (
    error instanceof DarvarehAPIError
  ) {
    console.error(
      "API error:",
      {
        status: error.status,
        code: error.code,
        message: error.message,
      },
    );

    process.exitCode = 1;
    return;
  }

  if (
    error instanceof DarvarehTimeoutError
  ) {
    console.error(
      "Request timed out",
    );

    process.exitCode = 1;
    return;
  }

  if (
    error instanceof DarvarehSDKError
  ) {
    console.error(
      "SDK error:",
      error.message,
    );

    process.exitCode = 1;
    return;
  }

  if (error instanceof Error) {
    console.error(
      "Unexpected error:",
      error.message,
    );

    process.exitCode = 1;
    return;
  }

  console.error(
    "Unknown error occurred",
  );

  process.exitCode = 1;
});

اجرای Type Check

npm run check

این فرمان Typeها را بررسی می‌کند، اما فایل JavaScript تولید نمی‌کند.

اجرای Example

npm run dev

در صورت صحیح‌بودن .env، پاسخ مدل در Terminal نمایش داده می‌شود.

Build SDK

npm run build

خروجی:

dist/
├── client.d.ts
├── client.d.ts.map
├── client.js
├── client.js.map
├── errors.d.ts
├── errors.js
├── index.d.ts
├── index.js
├── parsers.d.ts
├── parsers.js
├── types.d.ts
└── types.js

فایل‌های .d.ts Typeهای عمومی SDK را در اختیار پروژه مصرف‌کننده قرار می‌دهند.

ساخت Unit Test بدون درخواست واقعی

ازآنجاکه Client اجازه تزریق fetchImpl می‌دهد، می‌توانیم پاسخ API را شبیه‌سازی کنیم.

فایل tests/client.test.ts:

import assert from "node:assert/strict";
import test from "node:test";

import {
  DarvarehAPIError,
  DarvarehClient,
} from "../src/index.js";


test(
  "returns a typed chat completion",
  async () => {
    const fakeFetch: typeof fetch =
      async () => {
        return new Response(
          JSON.stringify({
            id: "chatcmpl-test",
            object: "chat.completion",
            created: 1234567890,
            model: "test-model",

            choices: [
              {
                index: 0,
                message: {
                  role: "assistant",
                  content: "پاسخ آزمایشی",
                },
                finish_reason: "stop",
              },
            ],

            usage: {
              prompt_tokens: 10,
              completion_tokens: 5,
              total_tokens: 15,
            },
          }),
          {
            status: 200,
            headers: {
              "content-type":
                "application/json",
            },
          },
        );
      };

    const client = new DarvarehClient({
      apiKey: "test-key",
      fetchImpl: fakeFetch,
    });

    const response =
      await client.createChatCompletion({
        model: "test-model",
        messages: [
          {
            role: "user",
            content: "سلام",
          },
        ],
      });

    assert.equal(
      response.id,
      "chatcmpl-test",
    );

    assert.equal(
      response.choices[0]
        ?.message.content,
      "پاسخ آزمایشی",
    );
  },
);


test(
  "throws a structured API error",
  async () => {
    const fakeFetch: typeof fetch =
      async () => {
        return new Response(
          JSON.stringify({
            error: {
              message:
                "Invalid API key",
              code:
                "invalid_api_key",
            },
          }),
          {
            status: 401,
            headers: {
              "content-type":
                "application/json",
            },
          },
        );
      };

    const client = new DarvarehClient({
      apiKey: "test-key",
      fetchImpl: fakeFetch,
    });

    await assert.rejects(
      () => {
        return client
          .createChatCompletion({
            model: "test-model",
            messages: [
              {
                role: "user",
                content: "سلام",
              },
            ],
          });
      },

      (error: unknown) => {
        return (
          error instanceof
            DarvarehAPIError &&
          error.status === 401 &&
          error.code ===
            "invalid_api_key"
        );
      },
    );
  },
);


test(
  "rejects invalid temperature",
  async () => {
    const client = new DarvarehClient({
      apiKey: "test-key",

      fetchImpl:
        async () =>
          new Response("{}", {
            status: 200,
          }),
    });

    await assert.rejects(
      () => {
        return client
          .createChatCompletion({
            model: "test-model",
            messages: [
              {
                role: "user",
                content: "سلام",
              },
            ],
            temperature: 3,
          });
      },

      /temperature must be between 0 and 2/,
    );
  },
);

اجرای تست:

npm test

این تست‌ها:

  • Request واقعی ارسال نمی‌کنند.
  • API Key واقعی نمی‌خواهند.
  • پاسخ موفق را بررسی می‌کنند.
  • خطای API را بررسی می‌کنند.
  • Validation ورودی را آزمایش می‌کنند.

چرا Dependency Injection برای fetch مفید است؟

اگر fetch مستقیماً و بدون امکان جایگزینی استفاده شود، تست Client ممکن است به Network واقعی وابسته شود.

با این طراحی:

fetchImpl?: typeof fetch;

در Production از fetch واقعی استفاده می‌شود و در تست، Function شبیه‌سازی‌شده جای آن را می‌گیرد.

این الگو مزایای زیر را دارد:

  • تست سریع‌تر
  • تست پایدارتر
  • حذف وابستگی به Network
  • حذف هزینه درخواست مدل
  • امکان شبیه‌سازی خطاهای مختلف
  • امکان تست Timeout و پاسخ نامعتبر

اضافه‌کردن Abort به Request

const controller =
  new AbortController();

const client = new DarvarehClient({
  apiKey:
    process.env.DARVAREH_API_KEY ?? "",
});

const promise =
  client.createChatCompletion(
    {
      model:
        process.env.DARVAREH_MODEL_ID ??
        "",

      messages: [
        {
          role: "user",
          content:
            "TypeScript چیست؟",
        },
      ],
    },
    {
      signal: controller.signal,
    },
  );

setTimeout(
  () => controller.abort(),
  1000,
);

await promise;

AbortSignal برای لغو درخواست از سمت مصرف‌کننده استفاده می‌شود. SDK علاوه بر آن Timeout داخلی نیز دارد.

در برنامه واقعی، Timer نمونه را بعد از تکمیل عملیات پاک کنید یا منطق Abort را در Lifecycle مناسب رابط قرار دهید.

Streaming در TypeScript

در SDK این مقاله stream عمداً روی false محدود شده است:

stream?: false;

زیرا پاسخ Streaming ساختار متفاوتی دارد و باید Chunkهای SSE را مرحله‌ای Parse کرد.

برای افزودن Streaming بهتر است Method جدا طراحی شود:

client.streamChatCompletion(...)

خروجی آن می‌تواند AsyncIterable باشد:

async function* streamChat(): AsyncIterable<string> {
  yield "بخش اول";
  yield "بخش دوم";
}

جداکردن API معمولی و Streaming باعث می‌شود Contract هر Method روشن‌تر باشد.

Runtime Validation چرا ضروری است؟

این Type فقط در زمان توسعه وجود دارد:

interface ChatCompletionResponse {
  id: string;
  choices: ChatChoice[];
}

اگر API پاسخ زیر را برگرداند:

{
  "unexpected": true
}

TypeScript در Runtime به‌صورت خودکار آن را رد نمی‌کند.

روش نامناسب:

const data =
  await response.json()
    as ChatCompletionResponse;

این Type Assertion بدون Validation است.

روش بهتر:

const data: unknown =
  await response.json();

const parsed =
  parseChatCompletionResponse(data);

در پروژه‌های بزرگ می‌توانید از Schema Validation استفاده کنید، اما اضافه‌کردن Library باید بر اساس نیاز، اندازه Bundle و معماری SDK تصمیم‌گیری شود.

Versioning در SDK

برای SDK عمومی، تغییر Typeها بخشی از API محصول است.

تغییرهای احتمالی Breaking:

  • تغییر نام Method
  • حذف Property
  • اجباری‌کردن Property اختیاری
  • تغییر Type خروجی
  • تغییر ساختار Error
  • تغییر Module Format
  • حذف Export
  • تغییر رفتار Timeout

پیش از انتشار نسخه جدید:

  • Type Check اجرا شود.
  • Unit Test اجرا شود.
  • Build تولید شود.
  • مثال‌ها اجرا شوند.
  • فایل Declaration بررسی شود.
  • Migration Note نوشته شود.
  • نسخه متناسب انتخاب شود.

مدیریت API Key در TypeScript

API Key را داخل کد قرار ندهید:

const client = new DarvarehClient({
  apiKey: "کلید واقعی",
});

برای Backend یا Script محلی:

const apiKey =
  process.env.DARVAREH_API_KEY;

اگر SDK در مرورگر اجرا شود، هر کلیدی که به JavaScript Frontend برسد باید قابل مشاهده فرض شود. برای اپلیکیشن عمومی، API Key درواره را روی Backend نگه دارید:

Browser
   │
   ▼
Backend شما
   │
   ▼
API درواره

SDK ساخته‌شده در این مقاله برای محیط Server-side یا ابزارهای محلی مناسب‌تر است. برای Frontend باید Client جداگانه‌ای بسازید که فقط به Backend خودتان متصل شود.

خطاهای رایج TypeScript

خطای implicitly has an any type

Parameter 'value' implicitly has an 'any' type

پارامتر Type ندارد:

function normalize(value) {
  return value.trim();
}

اصلاح:

function normalize(
  value: string,
): string {
  return value.trim();
}

خطای possibly undefined

const first =
  response.choices[0];

console.log(first.message.content);

با noUncheckedIndexedAccess ممکن است first وجود نداشته باشد.

اصلاح:

const first =
  response.choices[0];

if (!first) {
  throw new Error(
    "No response choice",
  );
}

console.log(
  first.message.content,
);

خطای Object is possibly null

const element =
  document.querySelector("#app");

element.textContent = "Hello";

اصلاح:

if (!element) {
  throw new Error(
    "Element not found",
  );
}

element.textContent = "Hello";

خطای Type is not assignable

Typeهای دو مقدار سازگار نیستند. به‌جای استفاده سریع از as any، Contract را بررسی کنید.

ممکن است یکی از این موارد عامل باشد:

  • Property اشتباه
  • Optional بودن مقدار
  • Union ناقص
  • Literal Type متفاوت
  • Generic اشتباه
  • پاسخ API نامعتبر
  • Type تعریف‌شده قدیمی

خطای Cannot find module

بررسی کنید:

  • Package نصب شده باشد.
  • Extension فایل در ESM درست باشد.
  • moduleResolution مناسب باشد.
  • مسیر Import صحیح باشد.
  • نام فایل و حروف کوچک و بزرگ هماهنگ باشند.

در پروژه NodeNext از Importهایی مانند زیر استفاده کرده‌ایم:

import {
  DarvarehClient,
} from "./client.js";

اگرچه فایل Source با پسوند .ts است، Extension خروجی Runtime در Import برابر .js نوشته می‌شود.

خطای fetch is not defined

در Runtimeهای قدیمی ممکن است fetch سراسری وجود نداشته باشد. از نسخه پشتیبانی‌شده Node.js یا یک fetchImpl سازگار استفاده کنید.

تعریف DOM Library در tsconfig فقط Type مربوط به Fetch را اضافه می‌کند و قابلیت Runtime را به محیط قدیمی اضافه نمی‌کند.

اشتباهات رایج در TypeScript

استفاده گسترده از any

any خطا را حل نمی‌کند؛ بررسی Type را کنار می‌گذارد.

استفاده زیاد از Type Assertion

اگر در بیشتر نقاط کد از as استفاده می‌کنید، ممکن است Type Design یا Runtime Validation مشکل داشته باشد.

تایپ‌کردن مستقیم پاسخ API

const data:
  ChatCompletionResponse =
    await response.json();

این کار پاسخ واقعی را در Runtime اعتبارسنجی نمی‌کند.

غیرفعال‌کردن strict

پروژه‌ای که با تنظیمات بسیار آزاد شروع شود، مهاجرت بعدی به Strict Mode سخت‌تر می‌شود.

ساخت Typeهای بسیار پیچیده

Type System قدرتمند است، اما Typeهای بیش از حد پیچیده می‌توانند درک کد و پیام خطا را دشوار کنند.

تکرار Typeهای مشابه

از Utility Type، Generic و Composition استفاده کنید، اما خوانایی را قربانی کوتاه‌شدن کد نکنید.

استفاده از enum برای هر مقدار ثابت

در بسیاری از موارد Literal Union ساده‌تر است:

type Status =
  | "pending"
  | "success"
  | "error";

Enum همچنان کاربرد دارد، اما باید رفتار Runtime تولیدشده آن را بدانید.

فرض ایمن‌بودن TypeScript در Frontend

TypeScript نمی‌تواند Secret قرارگرفته در Bundle مرورگر را مخفی کند.

بهترین روش‌های TypeScript

  • strict را فعال کنید.
  • از Inference مناسب استفاده کنید.
  • مرزهای عمومی را صریح Type کنید.
  • داده خارجی را unknown در نظر بگیرید.
  • پاسخ API را در Runtime اعتبارسنجی کنید.
  • از any فقط با دلیل روشن استفاده کنید.
  • Unionها را با Narrowing مدیریت کنید.
  • Switchهای مهم را Exhaustive کنید.
  • Errorهای اختصاصی بسازید.
  • Functionها را کوچک و تک‌هدف نگه دارید.
  • Type و Value Import را جدا کنید.
  • Unit Test بنویسید.
  • Build Output را بررسی کنید.
  • Secret را وارد Source یا Bundle نکنید.
  • نسخه‌های وابستگی را با Lock File کنترل کنید.
  • Typeهای عمومی SDK را بخشی از Contract محصول بدانید.

مسیر یادگیری TypeScript

مرحله اول

  • Typeهای پایه
  • Array
  • Object
  • Function
  • Type Inference
  • Type Annotation

مرحله دوم

  • Interface
  • Type Alias
  • Union
  • Literal Type
  • Intersection
  • Optional Property

مرحله سوم

  • Narrowing
  • Type Guard
  • unknown
  • never
  • Discriminated Union

مرحله چهارم

  • Generic
  • Constraint
  • keyof
  • Utility Type
  • Mapped Type
  • Conditional Type

مرحله پنجم

  • Module
  • ESM
  • TSConfig
  • Declaration File
  • Source Map
  • Project Reference

مرحله ششم

  • API Client
  • Runtime Validation
  • Error Design
  • Dependency Injection
  • Unit Test
  • Build و Package Publishing

پرسش‌های متداول

TypeScript چیست؟

TypeScript زبانی مبتنی بر JavaScript است که Static Type Checking و Tooling پیشرفته‌تری به آن اضافه می‌کند.

TypeScript مخفف چیست؟

TypeScript نام زبان است و معمولاً با TS کوتاه می‌شود.

آیا TypeScript از JavaScript سخت‌تر است؟

یادگیری Typeها مفاهیم جدیدی اضافه می‌کند، اما در پروژه‌های بزرگ می‌تواند درک Contractها، Refactoring و کشف خطاها را ساده‌تر کند.

آیا قبل از TypeScript باید JavaScript یاد بگیریم؟

بله، شناخت مبانی JavaScript برای استفاده درست از TypeScript ضروری است.

آیا مرورگر TypeScript را اجرا می‌کند؟

مرورگر معمولاً JavaScript را اجرا می‌کند. TypeScript پیش از اجرا به JavaScript تبدیل می‌شود یا توسط ابزار Build پردازش می‌شود.

Interface بهتر است یا type؟

پاسخ مطلقی وجود ندارد. Interface برای Object Contractها و Extension مناسب است؛ Type برای Union و Typeهای ترکیبی انعطاف بیشتری دارد. Consistency پروژه مهم‌تر است.

تفاوت unknown و any چیست؟

any بررسی Type را کنار می‌گذارد. unknown توسعه‌دهنده را مجبور می‌کند قبل از استفاده Type را بررسی کند.

Generic چیست؟

Generic Type یا Functionی است که Type را به‌صورت پارامتر می‌گیرد و ارتباط میان ورودی و خروجی را حفظ می‌کند.

آیا TypeScript پاسخ API را اعتبارسنجی می‌کند؟

خیر. Interfaceها و Typeها در Runtime حذف می‌شوند. پاسخ API باید جداگانه Parse یا Validate شود.

آیا SDK مقاله را می‌توان در مرورگر استفاده کرد؟

از نظر فنی بخش‌هایی از آن ممکن است قابل Bundle باشند، اما قراردادن API Key خصوصی در Frontend مناسب نیست. برای وب‌سایت عمومی، SDK را در Backend استفاده کنید.

چگونه مدل را تغییر دهیم؟

مقدار زیر را تغییر دهید:

DARVAREH_MODEL_ID=YOUR_MODEL_ID

فهرست مدل‌ها و قیمت جاری آن‌ها در صفحه مدل‌های درواره قرار دارد.

Base URL درواره چیست؟

https://api.darvareh.ir/v1

Endpoint Chat Completions چیست؟

https://api.darvareh.ir/v1/chat/completions

آیا TypeScript هزینه اجرای برنامه را افزایش می‌دهد؟

Typeهای TypeScript معمولاً از خروجی JavaScript حذف می‌شوند. هزینه Runtime بیشتر به JavaScript تولیدشده، Libraryها و معماری برنامه بستگی دارد.

فایل d.ts چیست؟

فایل Declaration ساختار Typeهای یک Package را برای TypeScript و ویرایشگرها تعریف می‌کند، بدون آنکه Implementation کامل JavaScript را در خود داشته باشد.

چک‌لیست پایان آموزش

پس از تکمیل مقاله باید بتوانید:

  • تفاوت JavaScript و TypeScript را توضیح دهید.
  • Typeهای پایه تعریف کنید.
  • Interface و Type Alias بسازید.
  • Union و Literal Type استفاده کنید.
  • Optional و Readonly Property تعریف کنید.
  • داده unknown را Narrow کنید.
  • Type Guard سفارشی بنویسید.
  • Discriminated Union طراحی کنید.
  • Generic Function بسازید.
  • از keyof و Utility Type استفاده کنید.
  • TSConfig سخت‌گیرانه تنظیم کنید.
  • Error اختصاصی طراحی کنید.
  • پاسخ API را در Runtime بررسی کنید.
  • fetch را برای تست تزریق کنید.
  • SDK را Build کنید.
  • Unit Test بدون Network اجرا کنید.
  • API هوش مصنوعی درواره را از TypeScript فراخوانی کنید.

جمع‌بندی

TypeScript فقط JavaScript همراه چند Annotation نیست. ارزش اصلی آن زمانی مشخص می‌شود که Typeها برای مدل‌کردن دقیق Contractها، وضعیت‌ها، Errorها و ارتباط میان ورودی و خروجی استفاده شوند.

در پروژه عملی این مقاله، یک SDK واقعی برای API درواره ساختیم که شامل موارد زیر بود:

  • Typeهای Request و Response
  • Client اختصاصی
  • Errorهای ساخت‌یافته
  • Timeout
  • AbortSignal
  • Generic Design
  • Runtime Parser
  • Dependency Injection
  • Unit Test
  • Build همراه فایل Declaration

SDK از Base URL زیر استفاده می‌کند:

https://api.darvareh.ir/v1

برای اجرای پروژه در درواره ثبت‌نام کنید، API Key بسازید و مدل مناسب را از صفحه مدل‌های درواره انتخاب کنید.

منابع تکمیلی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.