TypeScript не ўповільнены — занадто складныя типы ёсць.
Універсальныя супы, праця занадта рана пад типамі, стан неабяжнага флага і хитрасці на рэвэле типаў пагубная для шыроцы. Лепш выбіраць простыя інтэрфейсы, дзерыгіраваныя аюніі і разумнае выкарыстоўванне каштоўкаў tsc.
Як генерычныя супы, гімнастыка на адміністратыўных умовах і пранароджаная абстракцыя спамоцваюць змену швайна доставкі.
У парадоксальнай аналізе пасля спрынту младшы інжынер з’явівае, што дадзенне адной неабязковай полькі да існуючага API-пэйлоаду зайняла чатыры гадзіны. IDE паказвае прычыну: інтэрфейс, створаны з вялізных умоваў.
type ExtractNestedPayload<
T,
K extends keyof T,
U extends boolean = false
> =
T[K] extends (...args: any[]) => infer R
? R extends Promise<infer P>
? U extends true ? NonNullable<P> : P
: R
: T[K] extends Array<infer Item>
? Item
: never;
Чатыры слоі вялізных умоваў, трое генерычных параметраў і ланцоўка тэрнары, якой патрабуеяе дошка для записа. Памылкі кампайляра прыходзяць у вачоўкі дужа цвёрдага калеру:
Type 'ExtractNestedPayload<ApiResponses["users"], "data", true>'
is not assignable to type 'UserDTO'.
Types of property 'meta' are incompatible.Type 'unknown' is not assignable to type
'{ pagination: PaginationMeta }'.
Звычная скарга: сам TypeScript спамляе працю команды. Зазвычай так не ёсць. Проблема ў занадта складных абстракціях. TypeScript быў створаны як прагматычны інструмент — камі менша колькасць збоев пад час выканання, камі лепша автодаполненне. Але ў процесе выкарыстоўвання багатыя кодавыя базы ператворылі яго на сложную гэтыму задачу: гадзіны, витрачаныя на матэматычна «ідеальныя» генерыкі, каб ухиліцца ад калякоў ліній простых декларацыяў. Чым болей складным становіцца граф типаў, тым менш ён дапамагае людям, якія розробляюць функцыяны. Тып должен выражаць намер. Якщо для яго чытання трэбая умовныя типы, особлівасці інференціі, картаваныя типы, дыстрыбутивныя аюніі, рэкурсыўныя генерыкі і шлейка інструментаў, намер зникае, і трэба дебагаваць іншую систему. Ёсць шаблоны, якія пагубна паўтараюцца ў прыменэнні, і болей чыстыя альтернатывы, якія вяртаюць швальнасць розработкі.
1. Антишаблон «супы з генерыкамі»
Гэнерыкі выкарыстоўваюцца ў Promise<T>, Array<T> і Map<K, V>. Проблемы пачынаюцца тады, калі гнучкасць стае атрыбутам вышэйшага рангу. Больш гэнерычны код не значыць апошні. Возьмем компонент таблы:
// The Generic Soup Nightmare
interface TableProps<
TData,
TKey extends keyof TData,
TColumn extends ColumnDef<TData, any>,
TFilter extends Record<string, any> = Record<string, any>
> {
data: TData[];
keyExtractor: (item: TData) => TData[TKey];
columns: TColumn[];
initialFilter?: TFilter;
onRowClick?: (row: TData) => void;
}
Яны выглядаюць як інструменты, якія можна безліч разоў перадаць: формат даных, ключы рэядкаў, столбцы, фільтры. Кожная абстракцыя карае за сабой павышаны когнітыўны наклад. Месця вызыва змушваюць кампайлер адгадваць некалькі взаімаасоўваных параметраў. Незначны неяসাম্য у пропах можа не вказываць на конкрэтную проблему; адгадванне можа выйсці некоректным для всей структуры. Новы колега павінен з’ясаваць, чаму існуе TKey, чаму ён расшырюе keyof TData, як взаіднае дзейства гэнерыкаў столбцоў і фільтраў, а таксама што саме адгадаў кампайлер. Гэта вельмі вялікі тыржок для простага компонента таблы.
Гэтыя налогі выражаюцца у спадзе швайнае адзначэнне коду, паўтарычным процесе адкультуравання спэціялістаў і культуре, у якой толькі адна-две особы наважваюцца зменіць спяльна выкарыстоўваныя элементы інтерфейсу. Спад працэздольнасці є як тэхнічным, так і сацыяльным явам: колегі перестаюць пропанаваць маленькія паследоўныя змены, бо бояцца наследных нараканняў. Калі абстракцыя табелі патрэбуе документа з дизайнам, каб поясніць своія гэнерыкі, значыць абстракцыя вырасла за меры проблемы, якую ёй было задумана рашыць.
Сімптамы ў працэсе выконання є нуднымі і дорогімі. Перайменаванне адной лініі коду запускае дыягностыку, у якой згадваюцца некалякія параметры типу. Функцыя автадаптавання зупіняецца, пакуль служба мовы перэацэнюе структуру гэнерыкаў. Часы перацягвання тэстав на выяўленне типоў падымаюцца, а ніхто не стварае болей яснага модэлю домэна. Ніякі з гэтых расходаў не праказваецца ў тыповых аналізах „TypeScript проты JavaScript“; яны праказваюцца ў календарным часе.
Конкрэтная альтернатыва
Лепшае выбаранне — адзін параметр дадзейнаў і простыя інтэфейсы падтрымкі:
// Simple, readable, and instant compiler diagnostics
interface TableColumn<T> {
header: string;
accessor: (item: T) => React.ReactNode;
width?: string;
}
interface DataTableProps<T> {
data: T[];
columns: TableColumn<T>[];
rowKey: (item: T) => string;
}
Компанент застаецца можна выкарыстоўваць без таго, каб стаў «оракулам» типаў. Можна выкарыстоўваць — гэта не значыць бесканечна універсальна.
Можна выкарыстоўваць — гэта не значыць бесканечна універсальна.
Команды інодзе бярозца, што спрасцаванне універсальных рашэнняў змусіць іх выкарыстоўваць метод копіювання-выклейвання. На практыцы два чы тры сфокусаваныя варіянты табелей з яснымі параметрамі кращыя, чым адзін універсальны компанент, які ніхто не можа стварыць без проб і памылак. Дзеліцеся калектарамі для атрыбутаў візуальнага выкладчыка і CSS; заставьце публічныя параметры простымі. Тады кампайляр будзе паказваць точна тое поле, якое не паспявае, у замест на тое, каб аднаць чатыры зменныя выважэнняў у безканечную яму unknown.
2. Неранее застосаванне прынцыпа DRY у адзначэннях типаў
«Не павтараце сябе» ўжыткова для коду пад час выконання, але ўпэўненая яго прымена да тыпаў ёсць небяспечная. Бачыць падобныя поля — і команды ствараюць адні тып з іншаго:
// Over-abstracted type derivation
type RegisterFormValues =
Omit<
UserProfile,
'id' | 'createdAt' | 'updatedAt' | 'role'
> & {
passwordConfirmation: string;
termsAccepted: boolean;
};
Пазней энтытэць зміняецца:
interface UserProfile {
// ...
phoneNumber: string; // now required!
}
Утворены тып формы безшумна успадковуе обавязковы элемент phoneNumber, якога ніколі не было трэба ў процесе рэгістрацыі. Пасля гэтага трэба ўжо большых змян:
type RegisterFormValues =
Omit<
UserProfile,
'id' |
'createdAt' |
'updatedAt' |
'role' |
'phoneNumber'
> & {
phoneNumber?: string;
passwordConfirmation: string;
termsAccepted: boolean;
};
Кожная працэйка падвайвае незрозумеласць. Форма і энтытэць базы дадзеных зміняюцца з разных прычын; ўзаецанне іх стварае неспакойныя збоі.
Павтарэнне дешэўша, чым неправильная абстракцыя
Формулюйце кантракты окрема:
// Database Entity Contract
export interface UserProfile {
id: string;
email: string;
fullName: string;
phoneNumber: string;
createdAt: string;
}
// Registration Form Contract
export interface RegisterFormValues {
email: string;
fullName: string;
phoneNumber?: string;
password: string;
passwordConfirmation: string;
termsAccepted: boolean;
}
Калькі павтароўных полей коштаюць менше, чым крыхкі граф адведчэння. Якщо два тыпы зміняюцца з разных прычын, іх, верагодна, не трэба ўзаецаваць.
Якщо два типы змінююцца з разных прычын, іх, верагацейна, не трэба асоюваць.
Корыстны тэст на запах: чы можа менеджер продукту апісаць іх як адно панявленне? Рэяд з профілем пользователя ў храненні і форма рэгістрацыі на сторонцы маркетынгу рэдка калі маюць адноце па цыклу жыцця, правілах верыфікацыі чы власнасці. Калі ў яных з’яўляюцца разніцы, пахоўныя типы падсіліваюць гэтыя разніцы, прыводзячы да памилак у компіляванні, якія стояць далёка ад тых змян, што іх спрычынілі. Актыўныя інтэрфейсы робяць гэтыя разніцы виднымі і локальнымі. Памочныя функцыі для карэткавання — маленькія функцыі, якіе пераводзяць данні з аб’екта ў стандартны формат — дапамагаюць правдзіва пераводзіць данні пад час выканання, не з’ўязуючы ідэнтыфікаторы типаў назаўсёды.
3. Дзяленыя аюніі кращыя за неабавязковыя атрыбуты
Стан асінхроннага UI часта выглядае так:
// The "Optional Flag" Anti-Pattern
interface RequestState<T> {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data?: T;
error?: Error;
}
Потэм спожытчыкі ствараюць незаконныя комбінацыі — isSuccess без нехваткуючага data, або isLoading з усё ўсё застаўшымся кешаным адзінкам:
if (state.isSuccess && state.data) {
return <div>{state.data.name}</div>;
}
Необавязковыя флагі не кодуюць машыну станоў; яны кодуюць надзею.
Сіла дыскармінаваных аюніяў
Зробіце статус явным:
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
Адрасаванне стае вялікім і безпечным:
function RenderProfile({
state
}: {
state: AsyncState<UserProfile>;
}) {
switch (state.status) {
case 'idle':
return <div>Ready to load profile.</div>;
case 'loading':
return <LoadingSpinner />; case 'error':
return <ErrorMessage error={state.error} />; case 'success':
// TypeScript guarantees state.data exists here!
return <h1>Welcome, {state.data.fullName}</h1>;
}
}
Немагчымыя станы зникаюць з типу, таму багато захоўнічых пераканаў зникае з UI.
Немагчымыя станы зникаюць з типу, таму багато захоўнічых пераканаў зникае з UI.
Модэлі з неабяжнымі флагамі таксама плутаюць аналітыку і логаванне. Чы гэта запит быў успешны, якшо isSuccess ёсць правдзівы, але data не ўзначаны? Спецыяльныя з’еднанні вымагаюць, каб на гэты вопыт была дана адказ, калі стан ствараецца, а не калі молады інжынер рабіць здагадкі ў JSX. Редукаторы і асінхронныя обгорткі таксама становяцца яснейшымі: кожны переход вяртае цэлы варіант узамен таго, каб переключаць булевыя значэння, якія могу не збігацца.
4. Чылунок any проты unknown
Іспользованне any для таго, каб замовліць компайляр, пазбавляе нас пераканальных прынтасоў TypeScript на граніцы. Кращэ выбіраць unknown і звужваць типы за дапамою захоўнікаў пад час чытання даных з API або localStorage.
Заменіце any на unknown + захоўнікі типаў
// Safe parsing of unknown API or localStorage data
function parseStoredPreferences(
raw: unknown
): UserPreferences {
if (
typeof raw === 'object' &&
raw !== null &&
'theme' in raw &&
(raw.theme === 'light' || raw.theme === 'dark')
) {
return {
theme: raw.theme,
fontSize:
typeof (raw as any).fontSize === 'number'
? (raw as any).fontSize
: 14,
};
}
// Safe fallback default
return {
theme: 'dark',
fontSize: 14
};
}
Зовнішні даны застаюцься ненадзеянымі пакуль не будуць перавернуты; тады внутрэшны код атрымле конкретную форму.
Зовнішні даны застаюцься ненадзеянымі пакуль не будуць перавернуты; тады внутрэшны код атрымле конкретную форму.
any ўсё бол шкодзіць на межах модуляў, таму што ён парадуе выводы далей: адна значэння any з JSON.parse можа знешкодаваць всі перакананні ў цэлай функцыяй. unknown зупіняе гэты параду на самай входзе. Якщо пакеты дадзеных великія, выкарыстаўце бібліятэкі для перавернення схемы, а якщо формы маленькія і стабільныя — вручную напісаныя механізмы захавання. У будзь-якам случае, ядро домэна должна бачыць толькі перавернутыя типы.
5. Зважайце час перавернення типаў у CI
Калі рэдагувальнік работае повольна, спачатку зважайце час, прычынуя гэта не мове:
npx tsc --noEmit --extendedDiagnostics
Стежыце за колькасцю інстанціяў, файламі та часам. Часта домінуюць дорогі рекурсывныя умовні вырачэнні. Якщо система типаў карыстае многа ресурсаў пад час компіляцыі, яна должна правернуць гэтыя витраты.
Якщо система типаў карыстае многа ресурсаў пад час компіляцыі, яна должна правернуць гэтыя витраты.
Расширеныя діагностычныя засобы часта адкрываюць, што калька інстанціяў ствараецца ў некалько файлах — часта цэе рекурсывныя умовніы функцыі, імпортаваныя для зручнасці. Адменшэнне чы ўдаленне гэтых элементаў можа зарабіць калькі хвілін на кожны запуск CI. Стежыце за гэтым па часу так сама, як і за размахам пакета. Архітектура типаў, якая не можа паспрабаваць сваеія витраты на компіляцыю, зрэшты будзе адпавядаць за “спадзейную медлівасць TypeScript”, і каманда будзе менш інвеставаць у мову, якая ўсё ж такі захоўвае іх пад час выканання.
6. Проблема хітрасці на рэвэле типаў
Хітрасць таксама ёсць адна з пастак: типы, якія вытвараюць цэлыя інтерфейсы API на аднойчыне з іншымі типамі, пасля чаго ствараюць ўсё большу колькасць умовных выразаў, мапаваных типоў і рекурсій, пакуль ніхто не будзе іх адчыняць. Наявнасць павольнаў не ёсць прычыной выкарыстоўваць наймасівнейшыя функцыі.
Пораўняйце прямы доступ праз індэксы:
type UserName = UserProfile['fullName'];
з рекурсійным інструментам, які шукае кожную власць з значэнням у формате строкі ў будзь-ям об’екте. Якщо продукту патрэбна толькі UserProfile['fullName'], такая складная структура стварае рызыкі. Хорашая інжынерная практыка выбірае самы просты і зрозумелы інструмент. Тып, створаны за шэсць месцаў, должен сам сабе паводзіцься; фраза «не чапайце гэта» значыць, што абстракцыя вже зазнала невдачы.
7. Прагматычны маніфест TypeScript
1. Спачатку пішыце типы для людзей, а толькі потым — для кампайляра
Якшо колегі не можуць зрозумець вялічыню за менш чым мінуту, трэба яе спроставаць. Елегантнасць, якую розумее толькі автор, ёсць боргам.
2. Валідзіце копіюванне раштароўкі замест прычаснага супакоўвання
Не паспяшайце змінюваць інтэрфейс компаненту толькі каб перадаць знову трыя поля з несвязанай ентытэты. Раздзеляйце абавясанні, раздзеляйце типы.
3. Ожывайце дискримінаваныя аюніі для стану
Кодавайце рэальныя машыны стану з дискримінантом статусу, каб кампайляр адключыў немагчымыя веткі.
4. Ніколі не дазволяйце гэнерычным типам мяць больш двух параметраў
Тры чы ўжо больш гэнерычных типаў зазвычай значыць, што абстракцыя занадта шырока. Раздзеліце яе. эта гістэрычная правіла, а не закон — калі стосункі важкаяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяя
Медленныя процесы CI або затрымленыя службы падтрымкі мовы часта адвярцваюць структуру типоў, а не саму марку мовы. Складзіце прафіль, а потым спроставайце часта вжываемыя типы.
TypeScript павінен робіць код нудным
TypeScript працюе наяўней краща, калі не заважае: автодаполненне, безпечныя переработы коду, менш неспадзейкаў пад час выканання. Ён не павінен стаўць пры кожныя змены компонентаў у вопыт пазнавання. Найменш прыгожыя інтерфейсы — простыя бізнес-об’екты — часта ўсё ж такі маюць найбольшую цэннасць. Храніце типы простымі, практычнымі і спрыяюцымі рэальным концэпціям продукту, каб команда могла працаваць над розвіццю, а не над расшифравкай алгебры типоў.
Храніце типы простымі, практычнымі і спрыяюцымі рэальным концэпціям продукту, каб команда могла працаваць над розвіццю, а не над расшифравкай алгебры типоў.
Ретроспектывы становяцца кращымі, калі размова пераходзіць ад мовы праекту да выбораў дизайну: сколькі гэнерычных элементаў, насколькі ўзводжэння, насколькі чыстае ўстройства машыны станоў, як зовнішнія даны праходзяць у дапыт. TypeScript награджае такую чыстасцу быстрэйшымі адзывамі на значныя змены. Ён карае хитрасць незрозумелымі памылкамі. Свядома выберайце просты шлях, задокументавайце тыя некалькі прыглушаных типоў, якія дзейсна маюць значэнне, і не включайце складныя патэрны ў спільныя бібліятекі, з якімі вашы найнаветранейшы інжынеры павінны працаваць вядома з першага дня.
Калі здаецца, што змены блакуецца чераз типы, запытайцеся, чы рэпрэзентуе модель продукт. Часта рашэнням не ёсць глыбэйшыя умовы — гэта яснейшы інтэрфейс, роздзелены модуль або унія, якая называе станы, пра якія вы вже гаворыце пад час састаноўкаў. Самэ так TypeScript перестае быць некальківай навантажэнням і знова стае адной з пераваг.