Стандарты TypeScript, які робяць код строгім і гнучкім
Практычныя падзіркі на TypeScript: режым строгасці, атрыбут unknown over any, спарс-азертакцыі, адзінства строк, тип проты інтэрфейса, атрыбуты JSDoc, утыліты і дискримінаваныя адзінства.
Хорашы 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 для тых, хто жадае канонічных формулюванняў.
Спрыяйце гэтаму списку як жывойю даговоренасцю команды, а не як стацыяным правілам назаўсёды. Згодны.