Галоўная / Артыкулы / Працэспекты TypeScript, якія дзівуюць апытных разработчыкаў, і чаму

Працэспекты TypeScript, якія дзівуюць апытных разработчыкаў, і чаму

Структурныя типы, пераконтрольванне зайвых атрыбутаў, выкарыстоўванне ключавага слова as const, умовныя тыпы і мапаваныя тыпы, а таксама прынцыпы проектавання, якія ператвараюць іх у болей безпечны код.

4295 слоў

Большасць разработчыкаў пачынаюць з таго, што спрыяюць TypeScript як JavaScript з анотаціямі, а пасля сталкаюцца з практыкамі, якія не пасуюць да гэтага мнення: об’ект з дадатковымі полямі прыймаецца ў однам месцы, а адхіляецца ў іншым; тэза пра тип не «канвертуе» нічога, а readonly все равно дазволяе зменіць вялікі значэння. Пад базовымі анотаціямі знаходзится язык на рэвэле типаў з савоймі правіламі ўзаемнае сумеснасці, выведэння та вычыслення. Шырокі практыкум пояснюе гэтыя неспадзейкі адна за іншайю — ад стырання на час выканання та структурнага типавання да infer, шаблонных літэральных типаў та satisfies — і пасля ператварывае іх у практычныя прынцыпы дизайна, якія можна застосаваць у рэальных кодбазах.

Тыпы перестаюць існаваць, калі код выкананы

Ёсць інтэрфейс та об’ект, анотаваныя ім:

interface User {
  id: number;
  name: string;
}

const user: User = {
  id: 1,
  name: "Lakhveer"
};

Існуе падчына думаць, што запускаемая програма ведае, што user — это User. Але так не ўсё. Падчас кампілявання інтэрфейс усуваецца, і тое, што застаёцца, практычна ўскладніваецца да гэтага:

const user = {
  id: 1,
  name: "Lakhveer"
};

У часе выканання не існуе значэння User. TypeScript спачатку выканае статычны аналіз, пасля чыго генеруе JavaScript, і толькі гэты JavaScript доходзіць да двайчыка:

TypeScript
    ↓
Type Checking
    ↓
JavaScript Generation
    ↓
Browser / Node.js

Тыпы інформуюць кампілявер; яны не ўсё тыя об’екты, якія існуюць у часе выканання. Практычны наследак — TypeScript ніколі не перавяряе даны, якія прыходзяць званавні вашай программы. Анотацыя нижэй толькі падтвердзае тое, што вяртае сервер; нічога не перавяряецца:

const response: User = await fetch("/api/user")
  .then(res => res.json());

Для званавных дадзеных патрэбна перавярка ў часе выканання. Бібліятэкі шаблонаў, такія як Zod, апісваюць формат дадзеных раз і перавяряюцы яе, калі даны прыходзяць:

const UserSchema = z.object({
  id: z.number(),
  name: z.string()
});

const user = UserSchema.parse(data);

Прыгодны спосаб прыглядзець за раздзелам: кампайлер захоўвае ваш код, а перакананне пад час выконання захоўвае вашу аплікацыю. Адказ блога пра раздзеленне адной схемы Zod між фронтэндам на React і бэкендам на Node паказвае, як застосаваць гэта на обох канцах.

any, unknown і бяроза доказаў

any выключае перакананне

З any кожна з гэтых безсэнсовых операцый кампайлуецца без жадных працоў:

let value: any = "hello";

value.foo.bar.baz();
value();
value.notARealProperty;

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

function processUser(user: any) {
  console.log(user.name);
}

unknown выкалікае патрабаванне доказаў

unknown таксама прыме якое-небудзь значэнне:

let value: unknown = "hello";

Але яго прыменэнне безпасабедна не выйдзе:

value.foo;

Спачатку трэба яго сузіць, напрыклад да строкі:

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

альбо да числа:

if (typeof value === "number") {
  console.log(value.toFixed(2));
}

Разлік у падходзе лёгкая да апісання:

any
 ↓
"Trust me"

unknown
 ↓
"Prove it first"

Тады, калі вы сапраўды не ведаеце типу значэння, скорастіцеся да

unknown

замест

any

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

Сумаспадобнасць — гэта форма, а не назвы

Разработчыкі, якія працавалі з Java, C# чы C++, часта дзівуюцца, што такая прызначэння дазволена:

interface User {
  name: string;
}

const employee = {
  name: "Lakhveer",
  salary: 100000
};

const user: User = employee;

TypeScript выкарыстоўвае структурны типаванне: сумаспадобнасць залежыць ад таго, якія атрыбуты ў значэння, а не ад таго, як яно было заявлена. User патрэбны толькі гэтыя:

name: string

а employee мае іх, плюс ўсё інша. Логіка, якую выкарыстоўвае кампайлер, выглядае так:

User requires:
    name: string

employee has:
    name: string
    salary: number

Therefore:
    employee satisfies User

Або, як алгорытм прыняць рашэння:

Required properties
        ↓
Does object contain them?
        ↓
Yes
        ↓
Compatible

Занадто большая колькасць пераконтроўкаў атрыбутаў прыменяецца да свежых літэралайнаў

А тут сюрпрыз: прабаўка запісаць дадатковы атрыбут безпасэродзе ў літэрал об’екта адхоўваная:

interface User {
  name: string;
}

const user: User = {
  name: "Lakhveer",
  salary: 100000
};

З такой памылкай:

Object literal may only specify known properties

Аднак прызначэнне тых сабе дадзенняў чераз проміжную змянную прыймаецца:

const employee = {
  name: "Lakhveer",
  salary: 100000
};

const user: User = employee;

Прычына ў тым, што TypeScript адмаўляе дадатковыя атрыбуты у свежых літэралайнах об’ектаў, якія запісваюцца безпасэродзе ў момент прызначэння, як захопленне ад памылак у напісанні. Гэтыя пераконтроўкі не залежаць ад структурнай сумаспадобнасці. Таму фраза «TypeScript адмаўляе дадатковыя атрыбуты» прабачаецца толькі для літэралайнаў; калі об’ект вже праходзіць чераз змянную, дадатковыя атрыбуты не ўскладняюць роботу.

Тыпы літэралайнаў і выведзеныя юніёны

Точныя значэнні заместо шырокіх тыпаў

Змянную можна обмежыць конкрэтнымі значэннямі:

let direction: "left" | "right";
direction = "left";

Таму гэта прызначэнне не выйшла:

direction = "up";

У заявленні не сказана

direction: string

У яго сказана

direction must be EXACTLY:
"left"
OR
"right"

Што тачнасць значна павышае API. Функцыя развёрткі можа прыймаць толькі вядомыя сераверныя среды:

type Environment =
  | "development"
  | "staging"
  | "production";
function deploy(env: Environment) {
  // ...
}

і памылка написання стае памылкай кампілявання, а не невялікой памылкай развёрткі:

deploy("testing");

як const зменшае тое, што можна выважваць

Звычны літерал масі з стрэлкамі:

const colors = ["red", "blue", "green"];

выважваецца як

string[]

Даўшы as const:

const colors = ["red", "blue", "green"] as const;

выходзіць толькі туплам з чытальным даступам і літеральных типаў:

readonly ["red", "blue", "green"]

З гэтага тупла можна вывести ўнію, існуючы за дапамогою індэкса number:

type Color = typeof colors[number];

што дае:

type Color = "red" | "blue" | "green";

Это устраняе частае дуплікацыі. Без гэтаго трэба падтрымваць аб’юнію і масэвку, якія павінны быць синхронізаваны вручную:

type Color = "red" | "blue" | "green";

const colors: Color[] = [
  "red",
  "blue",
  "green"
];

За ўжыццём гэтаго масэвка становіцца едынай асаблівой інформацыяй, а тип автаматычна вылучваецца:

const colors = [
  "red",
  "blue",
  "green"
] as const;

type Color = typeof colors[number];

Гэты прынцып є загальным: калі система типа можа вылучыць інформацыю, не трэба яе запісваць два разы.

Ключавые словы, якія працуюць на рэвэле типа

typeof мае два завадзі

У JavaScript,

typeof value

являецца аператарыем часа выканання. Напрыклад,

typeof "hello";

дае

"string"

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

const user = {
  id: 1,
  name: "Lakhveer"
};

type User = typeof user;

Тут

User

стаецца

{
  id: number;
  name: string;
}

Тое ж ключавая слова, два контексты:

Runtime:
typeof value

Type system:
typeof variable

keyof ператварае ключы на аб’юнію

Якщо є інтэрфейс,

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

Гэты тип

type UserKeys = keyof User;

являецца

"id" | "name" | "email"

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

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

Выкліканне яго з існуючым ключам работае:

const user = {
  id: 1,
  name: "Lakhveer"
};

getValue(user, "name");

тады как ключ, якога няма, адхіляецца ў часы кампайлавання:

getValue(user, "salary");

Тып вярнення таксама ўточны: T[K] паводзіцца да типу таго конкрэтнага атрыбута.

Гэнерыкі спаяваюць значэнні між сабою

Тыповы гэнерык проста вяртае тое, што он прымеў:

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

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

function pair<T>(first: T, second: T): [T, T] {
  return [first, second];
}

таму гэты выклік прыймаецца:

pair(10, 20);

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

pair(10, "hello");

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

function first<T>(items: T[]): T | undefined {
  return items[0];
}

Для вызову на кшталт

const numbers = first([1, 2, 3]);

кампайлер адзвярцается рэзультатам:

number | undefined

Вычысленне типаў

Умовныя типы — гэта if на рывень типаў

Умовный тип выбірае аднам з двух рэзультатаў на адной з умоў:

type IsString<T> =
  T extends string
    ? true
    : false;

Такім чынам

type A = IsString<string>;

рэшаецца як

true

і

type B = IsString<number>;

рэшаецца як

false

Канцэптуальна вы пісаеце гэта, толькі гэта выпаловаецца ў кампайлеры, а не у вашай праграме:

if T is string
    return true
else
    return false

функцыя infer выдзейвае часткі типа

У ўмовнам типе infer адкрывае зменную типу, яку TypeScript запоўняе праз падбор. Цэта методыка пераімплементуе вбудованы ReturnType:

type ReturnTypeOf<T> =
  T extends (...args: any[]) => infer R
    ? R
    : never;

Калі яго застосаваць да рэальнай функцыі, ён выявляе тип аб’екта, які вяртаецца:

function getUser() {
  return {
    id: 1,
    name: "Lakhveer"
  };
}

type User = ReturnTypeOf<typeof getUser>;

Ментальны модэль гэтага процесу — це падбор шаблона:

Function
   ↓
infer R
   ↓
Extract return type

Большасць стандартных утилітных типоў ствараецца самэй так:

Тыпы-карткі ператвараюць кожную власць

Пачынаючы з інтэрфейсу,

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

тып-картка пераглядае сваіы ключы, каб стварыць версію толькі для чытання:

type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

альбо версію з можлівасцю абсэнцыі:

type OptionalUser = {
  [K in keyof User]?: User[K];
};

замест таго, каб практычна перапісваць кожную власць:

id?: number;
name?: string;
email?: string;

Утилітныя типы ствараюцца з гэтых элементаў

TypeScript прыносіць бібліятэку такіх дапаможных прыемаў:

Partial<T>
Required<T>
Readonly<T>
Pick<T, K>
Omit<T, K>
Record<K, T>
Exclude<T, U>
Extract<T, U>
NonNullable<T>
ReturnType<T>
Parameters<T>

Якшчы є такія модэлі,

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

Pick застаўляе выбраныя ключы:

type UserPreview = Pick<User, "id" | "name">;

стварае

{
  id: number;
  name: string;
}

а Omit іх адмаўляе:

type UserWithoutEmail = Omit<User, "email">;

Паўны спіс можна пазнакоміцца ў адгукі блога вбудованыя функцыйнае типы TypeScript.

never, narrowing і guards

never падтверджае, што вы адмаўілі ўсё

never — это тип значэння, якое не можа існаваць, напрыклад, рэзультат функцыі, якая завжды выклекае аберанцію:

function fail(message: string): never {
  throw new Error(message);
}

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

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

і switch, у якога ветка default перадае значэнне функцыі, якая прымеўа толькі never:

function handleStatus(status: Status) {
  switch (status) {
    case "loading":
      return "Loading";
    case "success":
      return "Success";
    case "error":
      return "Error";
    default:
      return assertNever(status);
  }
}

function assertNever(value: never): never {
  throw new Error("Unexpected value: " + value);
}

Калі апрацаваны ўсе варыянты, значэнне status да часу падачы на default ўжо зменшылася да never, таму выконваюцца перакрычанні типаў. Тепер заўважым, што хтось расшырюе цей союз:

type Status =
  | "loading"
  | "success"
  | "error"
  | "cancelled";

Непрацаваны варыянт "cancelled" доходзіць да assertNever; яго нельга прызначыць значэнню never, і кампайлер паказвае на кожны элемент switch, які трэба адкоректаваць.

Зменшэнне дыапазона значэнняў выконваецца па трыбухе керавання

Кампайлер стежыць за тым, як перакрычанні зменяюць можлівыя значэнні зменны.

function print(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

У першай гілце,

value

вядома, што

string

а ў другай — гэта

number

Спецыяльныя захоўнікі типаў

Вы можете навучыць кампайлер распазнаваць вашы сабственныя типы за дапамогою функцыі, якая вяртае тип-предыкат у формате value is User:

interface User {
  name: string;
}

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

Пасля таго, калі перакрыцэнне успехуе,

const data: unknown = getData();

if (isUser(data)) {
  console.log(data.name);
}

кампайляр спрыяе гэтой значэнню як

data: User

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

Моделюванне стану за дапамою разлічных аўтаматав

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

interface State {
  status: string;
  data?: User;
  error?: string;
}

Разлічныя аўтаматы моделююць кожны стан окрема, пазначаныя status:

type State =
  | {
      status: "loading";
    }
  | {
      status: "success";
      data: User;
    }
  | {
      status: "error";
      error: string;
    };

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

function render(state: State) {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return state.data.name;
    case "error":
      return state.error;
  }
}

Гэта выключае суперсечанні, такія як

status = success
error = "Something went wrong"

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

Выпалняе пераканваліванні без замены

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

const config = {
  port: 3000,
  host: "localhost"
} satisfies {
  port: number;
  host: string;
};

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

const config = {...} as Config;

as паведамляе кампайлеру прыймаць значэнне як той тип, і ён прыйме большасць значэнняў на ваша слова. satisfies замест таго прасіць кампайлер аб парадзенні паспаўлення. Калі ваша мета — пераканваліванне, а не замена кантролера пераканвалівання, лепш выбраць

satisfies

замест

as

Ліміты, якія падводзяць людзей

readonly ўзимае толькі верхній слой

Разглянем мы тип з атрыбутамі read-only, адны з якіх ўявляе сабою об’ект:

type User = {
  readonly name: string;
  readonly address: {
    city: string;
  };
};

Перазначэнне атрыбута верхньага рангу заблакана:

user.name = "New Name";

але зміна поля ўнутрь вкладанага об’екта все ж дазволена:

user.address.city = "Indore";

readonly дзейсніцца толькі да атрыбута, які ён пазначыў, а не рекурсыўна. Для глубокай незменнасці патрэбны рекурсыўныя картаваныя типы або механізмы пад час выконання, прычаму Object.freeze таксама ўзимае толькі верхній слой.

Асэрцыі не перакладаюць значэння

Гэтыя две асэрцыі компілююцца:

const value = "123" as unknown as number;

але нічога не перакладваецца. Пад час выконання,

typeof value

всё раве выдаецца

string

Якщо вам патрэбна цячка, перакладзіце яе явна:

const value = Number("123");

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

Необавязковыя элементы не завжоды ўзначаюць «не заданыя»

Необавязковая атрыбут:

interface User {
  name?: string;
}

зазвычай значыць, што ключ можа не існаваць, таму об’ект будзе порожнім

{}

являецца правільным, так сама як і

{
  name: "Lakhveer"
}

Але тое, чыі явны

{
  name: undefined
}

дозволены, залежыць ад налашоўкаў. Актывацыя

{
  "exactOptionalPropertyTypes": true
}

дазволяе кампайляру розразліваць гэтыя два варыянты. Гэта мае значэнне для API, дзе

property missing

і

property explicitly undefined

маюць разныя значэнні, напрыклад у запытку PATCH, дзе відсутняе поле означае «не зменяць», а явнае значэння — «аспіраваць яго».

Індексаванне можа сховаць «не заданыя» значэнні

TypeScript намеравана не прыменяе захоплення проты кожнай памяткі часу выканання. Чытанне па за межы массива:

const numbers = [1, 2, 3];
const value = numbers[100];

пад стандартнымі налашоўкамі тракуецца так, як будзь-якая цыфра завжды існуе. Актывацыя

{
  "noUncheckedIndexedAccess": true
}

Забезпечвае доступ

numbers[100]

Адказвае як

number | undefined

Што вымагае адрабатавання ситуацыі, калі чаго-то не хапяце.

Тыпы страк і асоцыяцыйных стосункоў

Тыпы шаблонных літэралайнаў

TypeScript можа ствараць тыпы страк з іншых тыпоў страк:

type EventName =
  `user:${"created" | "updated" | "deleted"}`;

Што раскрываецца ў

"user:created"
"user:updated"
"user:deleted"

Тая ж тэхніка можа выкліквачыць опис маршрутаў:

type HttpMethod = "GET" | "POST";

type Endpoint =
  `${HttpMethod} /users`;

Такім чынам, дзеяныя значэнні ўсё тое ж

"GET /users"
"POST /users"

Складанне элементаў у вычысляваныя API

Этые функцыі складаюцца з:

keyof
typeof
conditional types
mapped types
template literals
infer
generics

Пачніце з карточкі, якая супарабатоўвае назвы зьведанняў з тыпамі паданых дадзеных:

type EventMap = {
  userCreated: {
    id: number;
  };

userDeleted: {
    id: number;
  };
};

Універсальны эмітэр можа з’яўляць кожную назву зьведанняў з яе паданымі дадзенымі за дапамогою keyof і індексаванага доступу:

class EventEmitter<Events extends Record<string, unknown>> {
  on<K extends keyof Events>(
    event: K,
    callback: (payload: Events[K]) => void
  ) {
    // ...
  }

emit<K extends keyof Events>(
    event: K,
    payload: Events[K]
  ) {
    // ...
  }
}

Эмітаванне вядомага зьведанняў з правымі паданымі дадзенымі компілюецца:

const emitter =
  new EventEmitter<EventMap>();

emitter.emit("userCreated", {
  id: 1
});

У той час як неправыя паданыя дадзеныя адхоўляюцца:

emitter.emit("userCreated", {
  name: "Lakhveer"
});

Кампайляр тепер разумее зв’язак між назвай прыбутку і дадзеннямі, якія павінны яго супрацаваць.

Режым строгасці

Професійны проект зазвычай павінен пачыцца з:

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

Этот адзін флаг умоžняе выкананне няваткоў пераказаў, уключаючы:

strictNullChecks
noImplicitAny
strictFunctionTypes
strictPropertyInitialization
useUnknownInCatchVariables

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

Зробіць некоректныя станы непрыемлемымі

Разглядзіце жыцёвы цикл запиту, які моделюецца як унія:

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

Пораўняйце яго з дызайнам на базе флагаў і неабавязковых поль:

interface RequestState {
  loading: boolean;
  data?: User;
  error?: string;
}

Другі варыянт дазволяе наяўнасць нелогічных ситуацый, такіх як

{
  loading: true,
  data: user,
  error: "Something failed"
}

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

Прынцыпы дизайна для практычнага викорыстання TypeScript

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

Вважайце any выключным варыянтам

Замест таго, каб

function process(data: any) {
  // ...
}

выбіраць

function process(data: unknown) {
  // validate/narrow first
}

а калі вы ведаеце ўсю структуру, тым лепяй

function process(data: User) {
  // ...
}

Вжывайце any толькі тады, калі вы абоўсюдзе розумееце, што самэго вы пасвячаеце.

Дазвольце механізму адгадвання разбірацца з очывіцамі

Такія анотацыі ствараюць зайвы шум:

const name: string = "Lakhveer";
const age: number = 28;

Кампляйзар уже знае:

const name = "Lakhveer";
const age = 28;

Зберагаюце явныя типы там, дзе яны задаюць умовы відносін межаў.

Нехай типы выражаюць намер

Простая строчка практычна нічога не гаворыць:

function process(value: string) {}

Імёваны тип пазначае, што значыць даныя:

type UserId = string;
function processUser(userId: UserId) {}

Адна працоўная абмежэння: аляска на кшталт UserId = string задае намер, але не заважае пасылаць ProductId там, дзе трэба UserId, адколі оба ёсць проста строчкамі. Якщо плутанне іх ёсць рэальным рызыкам, тип з іменем дапамагае ў цім контролі.

Трымайце типы блізка да сферы дзейнасці

Падпис, створаны з суровых строчак:

function createOrder(
  userId: string,
  productId: string,
  status: string
) {}

стае значна ясней з типамі сферы дзейнасці:

type OrderStatus =
  | "pending"
  | "paid"
  | "cancelled";

function createOrder(
  userId: UserId,
  productId: ProductId,
  status: OrderStatus
) {}

Тепер кампляйзар розумее вашу бізнес-тэрміналогію, а не толькі прымітныя формы.

Вядомае прывалюванне лепш за булевы флагі

Незалежныя булевыя значэння дазваляюць ствараць немагчымыя комбінацыі:

interface State {
  loading: boolean;
  success: boolean;
  error: boolean;
}

Юнія дазваляе наявнасць толькі аднаго стану за раз:

type State =
  | "loading"
  | "success"
  | "error";

Перайдзіце на дискримінаваную юнію, калі стану патрэбны савастныя даны.

Параболіце на граніцах системы

Кампайляр не можа гарантаваць нічога, што прыходзіць з зявні:

API
Database
User input
Environment variables
Files
Third-party services
JSON
Local storage

Спрыятліваце ўсё гэта як ненадзяжна і направляйце ўсё через адну лінію обробкі:

External data
     ↓
Runtime validation
     ↓
Trusted typed data
     ↓
Application logic

Параболіцаныя даны стаюць надзяжнымі, типаванымі данымаў, і толькі яны доходзяць да логіки прыемлена.

Утрымайцеся ад занадто сложнага проектавання

Вы можете стварыць чрэзнаадзвычайна складныя типы, але схема на кшталт

type Something<T, U, V, X extends ...> = ...

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

Проектаваце API для правільнага выкарыстоўвання

Выконанне вызова з іншымі флагамі пазычэння лёгкая прычына для памылак:

createUser(
  "Lakhveer",
  "admin",
  true,
  false,
  undefined
);

Об’ект з параметрамі, заданымі як типы, сам по сабе ўсё адзначае і забезпечвае набліжна лепшую автодополненне:

createUser({
  name: "Lakhveer",
  role: "admin",
  active: true
});

Складаце элементы заместо стварэння вялічзіных інтэрфейсаў

Одны інтэрфейс з дзесяткамі поль

interface User {
  // 50 properties
}

які ўскладнюе розумеенне па супэрнаціям з меншымі концэптамі:

type Identifiable = {
  id: string;
};

type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type User =
  Identifiable &
  Timestamped & {
    name: string;
  };

Зробіце кампайляр часткай вашай стратэгіі тэставання

Тыпы не заменяюць тэсты, але ўсуваюць цэлыя категоріі багоў яшчэ до ўжывання тэстаў. Як толькі

type PaymentStatus =
  | "pending"
  | "paid"
  | "failed";

дадаецца новы элемент, напрыклад

"refunded"

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

У вашай галаве раздзеляйце час кампайлявання і час выконання

Задайце сабе, у якам слое вы працуеце. Гэта толькі час кампайлявання:

interface User {
  id: number;
}

Это пераконтроль у часу выканання:

if (typeof value === "object") {
}

А гэта — пераверыканне зовнішняй дадзейны на часу выканання:

UserSchema.parse(data);

Чытайте выданы JavaScript

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

Глыбокае разуменне JavaScript

TypeScript стоіць на базе JavaScript, таму фундаменталы застаюцца важлівымі:

Closures
Promises
Event Loop
Prototypes
this
Modules
Destructuring
Async/Await
Objects
Arrays
Functions
Hoisting
Scopes

Робіце tsconfig.json цярпяча

Не копіюйце настройкі слепа. Зрозумейце, што дае і чаго кошта кожны пункт:

{
  "strict": true,
  "noUncheckedIndexedAccess": true,
  "exactOptionalPropertyTypes": true,
  "noImplicitOverride": true
}

Кожны флаг змінюе баланс межа безпекай і зручнасцю; напрыклад, noImplicitOverride вымагае наявнасці override для кожнага методу, які заменяе метод з базовай класы.

Многослойны ментальны модэль

Дапамагаець уявіць TypeScript як два паралельныя шары: JavaScript у часе выконання і систему типаў у часе кампілявання, пры чым функцыяны на рэжыме типаў будуюцца адно на іншам:

                TypeScript
                     │
        ┌────────────┴────────────┐
        │                         │
   JavaScript                 Type System
        │                         │
 Runtime Behavior          Compile-Time Safety
        │                         │
 Browser / Node            Type Relationships
                                  │
                       ┌──────────┼──────────┐
                       │          │          │
                    Generics    Unions    Inference
                       │          │          │
                    keyof      never     conditional
                       │          │          │
                    mapped     guards      infer
                       │          │          │
                       └──────────┴──────────┘

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

Ключовыя выводы

Функцыяны, якія найбольш варта опанаваць, — гэта не тыя, якія выглядаюць найболей прыгожа:

Generics
Unions
Narrowing
Inference
keyof
typeof
Mapped Types
Conditional Types
infer
Discriminated Unions
never
unknown
satisfies
Template Literal Types
  • Тыпы стыраюцца у часе выконання, таму зовнішнія данні завжды патрабуюць верыфікацыі.
  • Сумеснае працаванне ўсёга адбываецца на структурнам рэжыме, пры чым дапаможная перакалька выканаўця ўжо толькі для новых літералайнаў аб’ектаў.
  • as const, typeof, keyof, умовныя та картаваныя типы дазволяюць адгэтуваты типы замест таго, каб іх дублюваць.
  • Калі ваша мета — пераканальванне, а не замена, лепш выбіраць unknown замест any та satisfies замест as.
  • Ўжывайце дискримінаваныя аб’юнкціі та never, ўсёляку, каб кампайлер могаў сказаць вам, чы можа практычна выйсці такі стан.
  • Мета не ў найболей сложных типах, а ў кодзе, дзе кампайлер адпавядае на пытанне «чы можа выйсці такі стан?» яшчэ перад тым, как програма запускаецца. Ўжывайце TypeScript для стварэння болей безпечнага коду, а не проста для опису коду, які вы вялі ўжо.

    Спаднёе чытанне

  • Дзесяць шаблонав TypeScript, які ператвараюць багі пад час выканання на галаты кампайлявання — Дазвольце пазнакоміцца з дзесяцьма тэхнікамі TypeScript, такімі як дысцримінаваныя аб’юніі, функцыі satisfies, типы branded та infer, якія дазволяюць кампайляру адхіліць некоректныя станы ўсё раней, чым ваш код будзе выданы.
  • Што ловіць кампайляр TypeScript, а што працягвае працаваць у JavaScript — Порашчыраваймо JavaScript і TypeScript: адгадванне типаў, анатазыі, прымітывы, any, аб’юніі та функцыі з пазначэннямі типаў, а таксама тое, што на самай праце фіксуе і выдае tsc.