Галоўная / Артыкулы

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

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

Выдавец: REACTAPP.TOP7 хв чытання1535 слоў

Крыніцы: **TypeScript, with Opinions**

Хорашы 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 для тых, хто жадае канонічных формулюванняў.

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