Працэспекты TypeScript, якія дзівуюць апытных разработчыкаў, і чаму
Структурныя типы, пераконтрольванне зайвых атрыбутаў, выкарыстоўванне ключавага слова as const, умовныя тыпы і мапаваныя тыпы, а таксама прынцыпы проектавання, якія ператвараюць іх у болей безпечны код.
Большасць разработчыкаў пачынаюць з таго, што спрыяюць 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, якія ператвараюць типы на рэальную захопчыню ад багоў — Дазвольце вам дазнацца, як satisfies, tagged unions, never checks, unknown, derived types і branded IDs дапамагаюць TypeScript ловіць рэальныя багі ў часе компілявання, а не пад час роботы програмы.
- Модэлюванне доменав у TypeScript: за межамі базовых анотацый типоў — Дазвольце вам пазнакоміцца з практычнымі падходамі ў TypeScript — ад unknown vs any да discriminated unions і satisfies — якія дапамагаюць модэлюваць правільныя станы дадзеных, а не проста якіх-небудзь атрыбутаў.