Галоўная / Артыкулы / Стандарты TypeScript, які робяць код строгім і гнучкім

Стандарты TypeScript, які робяць код строгім і гнучкім

Практычныя падзіркі на TypeScript: режым строгасці, атрыбут unknown over any, спарс-азертакцыі, адзінства строк, тип проты інтэрфейса, атрыбуты JSDoc, утыліты і дискримінаваныя адзінства.

1535 слоў

Хорашы TypeScript должен зменшыць шанс на апрадку некоректнага коду, не ператвараючы кожны файл у рутыну. Наведзеныя нижэй стандарты спрыяюць простейшым, болей безпечным і лёгкім у адтрыманні форматам, заўсёды зберагаючы гнучкасць, якая робіць TypeScript корыстным ў павсякдзеннай праце.

Короткая сутнась

  • Чытабельна пісаць спефікаціі кантрактаў на рубежах модуляў і компонентаў.
  • Дазволіць апарату перагляду выважваць прыватныя памочнікі і локальныя зменныя.
  • Робіць выключэння паведамленняў мінімальнымі і цялеспрямаванымі.
  • Вжываць вялікія аліясы і утварэнні з канцэптуальнымі назвамі, прычыму не ствараць новых.
  • Вжываць літэральныя аб’еднання і варыянты з атрыбутамі, калі станы є альтэрнатывамі.

Увёліце режым строгасці

Заўжды увёліце строгі перагляд у tsconfig.json (канцэптуальна):

{
  "compilerOptions": {
    "strict": true
  }
}

Параметры строгасці дапамагаюць выяўляць распашчастыя помылкі раней і падтрымваюць высокі стандарт правільнасці ў зростаючай базе коду.

Утрымайцеся ад типа any

Калі тип значэння няясны, лепш выбраць unknown замест any, тады для яго выкарыстання спачатку трэба яго адгрануць:

// bad
function processValue(value: any) {
  value.toLowerCase(); // No error, but might crash at runtime
}

// good
function processValue(value: unknown) {
  if (typeof value === 'string') {
    value.toLowerCase(); // OK, we've verified it’s a string
  }
}

Ігнараванне пераканалень типа

Калі справаўна патрэбна адказа з пераканалень, трэба зменшыць меры захоплення як можно больш.

Выкарыстанне as any для адказа ўжо аднам выразу часта краща, чым @ts-ignore, таму што такое пераканаленне паўтараецца толькі для таго выразу, тады калі @ts-ignore можа замовліць выкарыстанне цэлай наступнай лініі.

// third-party library with incomplete type definitions
import { someExternalLib } from 'external-lib';

const result = (someExternalLib.complexMethod() as any).undocumentedProperty;

У коде, створанам самой аплікацыяй, лепш выправіць типы, дадаць захоплення або пераканаліць зовнішнія даны (напрыклад, за дапамою Zod), замест таго каб проста замовіць выкарыстанне пераканалень.

Выкорыстоўваце @ts-expect-error, калі вы знаеце больш, чым можа падтвердзіць пераканальнік:

  • У працы з @ts-ignore компіляцыя не будзе выканана, якщо бяда з’явіцца неспадзевана.
  • Это дапамагае заставіць намераваныя замовленні пераканалень застацца чыстымі пад час адзорткі.
<div
  style={{
    // @ts-expect-error — vars are valid CSS properties
    '--size': `${dimensions.size}px`,
  }}
/>

Існуючыя асэрцыіі выкорыстоўваеце розумна

as асэрцыіі перакрываюць механізм перагляду. Калі гэта можліва, вярніцеся да парадкав пераканання ў часе выконання:

// bad
const myCanvas = document.getElementById('canvas') as HTMLCanvasElement;

// good
const myCanvas = document.getElementById('canvas');
if (myCanvas instanceof HTMLCanvasElement) {
  const ctx = myCanvas.getContext('2d');
}

Вярніцеся да типовых захоўнікаў замест асэрцый

Захоўнікі прыменяюць ся толькі пасля успеху пераканання ў часе выконання — гэта болей надзеяны спосаб у пораўнанне з простым as:

type User = {
  id: string;
  name: string;
};

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof value.id === 'string' &&
    'name' in value &&
    typeof value.name === 'string'
  );
}

function greet(value: unknown) {
  if (isUser(value)) {
    // value is User here - no assertion needed
    console.log(`Hello, ${value.name}`);
  }
}

Выкорыстоўваеце асэрцыю толькі тады, калі пераканання ў часе выконання немагчыма або беспараднэ — зазвычай калі параметры трэціх сторон не паспадзяюць з загальным шаблонам формы:

<OTPInput
  // Our form pattern expects a traditional change handler but this third-party
  // component provides the text value as its only callback argument. We need
  // to manually create an event-like object so validation works as expected
  // for consumers.
  onChange={(value) => {
    props.onChange?.({
      target: { value },
    } as ChangeEvent<HTMLInputElement>);
  }}
/>

Вярніцеся да з’еднанняў літэрактурных строк замест энумерацый

Для коду прыкладнага праграмавання з’еднанняя літэрактурных строк зазвычай кращыя за энумерацыі: няма об’екта ў часе выконання, простыяя процесы выдачы дадзеных і лепшая падтрымка для серіявання:

// bad
enum Environments {
  DEV = 'dev',
  TESTING = 'test',
  STAGING = 'staging',
  PRODUCTION = 'prod',
}

// good
type Environments = 'dev' | 'test' | 'staging' | 'prod';

Заставайцеся з энумерацыямі, калі трэба об’ект у часе выконання, калі існуючы API вжо выкарыстоўвае ўсі энумерацыі, або калі стандарты команды гэта выкаланяюць.

Выводзіце з’еднанняя з значэнняў

Калі значэнні вялікі ў часе выканання, аб’еднанне трэба атрымваць з аднаго адпаведнага джерела:

const ENVIRONMENT_META = {
  dev: {},
  test: {},
  staging: {},
  prod: {},
};

type Environments = keyof typeof ENVIRONMENT_META;
//   ^? 'dev' | 'test' | 'staging' | 'prod'

Форма масів:

const ENVIRONMENTS = ['dev', 'test', 'staging', 'prod'] as const;
type Environments = (typeof ENVIRONMENTS)[number];
//   ^? 'dev' | 'test' | 'staging' | 'prod'

Патрабуецца асэрцыя const, каб маса застаўалася туплам літэралаў, а не string[].

Вядзейце type замест interface

Для большай часткі коду прыложэння type пакрывае форматы об’ектаў, а таксама аб’еднанні, перасеканні, карэльваныя та умовныя типы. Ён таксама не можа быць перадзвануты, каб утрамадзілася випадковая з’еднанне:

// when required
interface Person {
  name: string;
  email: string;
}

// preferred
type Person = {
  name: string;
  email: string;
};

Выбір адной формы скараць непатрэбную зміну межаў між перакрытымі функцыямі.

Іспользуйце interface толькі тады, калі яго функцыяны маюць значэнне

Публічныя бібліятэкі можаць намерна экспортаваць interface, каб корыстувальнікі моглі ўдосконаліць яго:

// library
export interface Theme {
  colors: Record<string, string>;
}

// consumer: extend the interface without modifying the original source
declare module 'my-library' {
  interface Theme {
    spacing: Record<string, number>;
  }
}

Документавацыя прынтакоў компаненту

Ўзайце JSDoc (/** ... */) для публічных або незрозумелых атрыбутаў — меты, працэспектаў, абмежэнняй — калі контекст яшчэ свежы:

type SearchFieldProps = {
  /**
   * A unique identifier for the field. Consider [useId](https://react.dev/reference/react/useId) to generate a unique ID.
   */
  id?: string;
  /**
   * The label of the field. For "visually hidden" labels, use the `aria-label`
   * attribute.
   */
  label?: string;
  /**
   * @deprecated Use `label` instead.
   */
  title?: string;
  /**
   * Handler that is called when the field is submitted.
   */
  onSubmit?: (value: string) => void;
  /**
   * Handler that is called when the clear button, or <Escape> key, is pressed.
   */
  onClear?: () => void;
} & AriaLabellingProps;

Асаблівасці дакументацыі

Адкладзіце детальную дакументацыю, калі:

  • Компонент выкарыстоўваецца толькі раз і малая верыгатнась, што ён будзе вяртацца
  • Ён ўжо є прыватным элементам дакументаванага родніка, чыяй спецыфікацыя вже пояснюе працэспекты

Калі не ведаеце, вольбіце яснась і будзіце адносовымі.

Павторны выкарыстоўванне існуючых типаў

Зробіце публічныя API чытабельнымі; нехай внутрашняе здольнасць абрабатвае внутрашняе. Перш чым ствараць новую абстракцыю, шукайце існуючы тип, атрыбут чыя-небудзь прадметную функцыю, каб іх можна было павторна выкарыстаць і ўтримаць супараднась у спакаянам коде.

Адчыненне атрыбутаў

Індексаваныя адчыненняя запоўнююць тип атрыбута з існуючага псевданіма:

import type { ThingProps } from '../thing';

type SpecialThingProps = {
  ...
  label?: ThingProps['label'];
  type?: ThingProps['type'];
};

function SpecialThing(props: SpecialThingProps) {}

Вольбіце перасеканні для складання

Складайце типы без дуплікацыі поль (і ўсіх адпаведных дакументацый):

type Person = { name: string; email: string };
type User = Person & { id: string };
//   ^? { name: string; email: string; id: string }
import type { ThingProps } from '../thing';

type SpecialThingProps = {
  ...
} & Pick<ThingProps, 'label' | 'type'>;

function SpecialThing(props: SpecialThingProps) {}

Деталі рэалізацыі Infer

Fункцыя Infer падтрымлеўа сінхронізацыю внутраніх тыпоў з значэннямі:

function initContext() {
  return {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
  };
}

type Context = ReturnType<typeof initContext>;
//   ^? { name: string; email: string }

ComponentProps вылучае атрыбуты з компонента React, калі бібліятэка іх не экспортуе:

import type { ComponentProps } from 'react';
import { RouterProvider } from 'react-aria-components';

type RouterProviderProps = ComponentProps<typeof RouterProvider>;
//  {
//    navigate: (path: Href, routerOptions: RouterOptions | undefined) => void;
//    useHref?: (href: Href) => string;
//    children: ReactNode;
//  }

Parameters вяртае туплу аргументаў, якія можна індексавацыяваць:

// library code

declare function someLibFn(
  value: number,
  options: {
    minimumFractionDigits: number;
    maximumFractionDigits: number;
  }
): void;

// consumer code

import { someLibFn } from 'some-lib';

type LibFnValue = Parameters<typeof someLibFn>[0];
//   ^? number
type LibFnOptions = Parameters<typeof someLibFn>[1];
//   ^? { minimumFractionDigits: number, maximumFractionDigits: number }

Іспользаванне вбудованых типоў-прыемнікаў

Тыпы-прыемнікі пакрываюць частыя трансфарматы і ўпінаваныя для новых членаў команды:

type User = {
  id: string;
  name: string;
  email: string;
};

type UserPatch = Partial<User>;
type UserDetails = Omit<User, 'id'>;
type UserIdentity = Pick<User, 'id' | 'email'>;

Запісваць спецыяльны прыемнік трэба толькі тады, калі назва трансфарматы відпавядае рэальнай концэпцыі, а не проста для заўсёды эканоміі клавіш.

Іспользаванне дискрымінаваных аюній

Аюніі з пазначкамі моделююць альтернатывы, што дазволяе пераканацца ў ўсепрацэсаванасці:

type Actions =
  | { type: 'login'; username: string }
  | { type: 'logout'; reason: 'session-timeout' | undefined }
  | { type: 'update'; id: string; data: Record<string, unknown> }

Спяльнае літэральнае поле (type або kind) дазволяе TypeScript безпечна сузаць кожную галузь.

Падсумак

Адэкватна практыка перамагае будзь-які адзін правіл. Даскайцеся на стандарты, задокументавайце асаблівасці і оптымізавайце для наступнага чытача:

  • Чыстыя умовы краевых случаў
  • Выведзеныя внутршніе элементы
  • Мінімальныя способы скасавання
  • Спакульныя псевдонімы і засобы
  • Аб’еднанні з атрыбутамі, дзе незаконныя комбінацыі павінны з’явіцца

Якщо ўжываць іх разам, гэтыя прыкметы робяць TypeScript адказным інструментам для проектавання, а не трыбутам: публічныя API застаюцца чыстымі, внутршніе элементы — адсорбаванымі, а застосоўванне any чы ў as стае рэдкім, ператвараючыся на асаблівасці, якія можна пераглядзець, а не на стандартны спосаб выхаду.

Гэтыя прамавы знаходзяцца ў афіцыйным рэхбуку TypeScript і документах JSDoc для тых, хто жадае канонічных формулюванняў.

Спрыяйце гэтаму списку як жывойю даговоренасцю команды, а не як стацыяным правілам назаўсёды. Згодны.