TypeScript не ўповільнены — занадто складныя тыпы ёсць.
Універсальныя супы, праця занадта рана пад тыпамі, стан неабяжнага флага і хітрасці на рэвэле тыпаў пагубная для шыроцы. Лепш выбіраць простыя інтэрфейсы, дзерыгіраваныя аюніі і разумнае выкарыстоўванне каштоўкаў tsc.
Крыніцы: TypeScript Isn’t Slowing Your Team Down Your Over-Engineered Types Are
Як генерычныя супы, гімнастыка на адміністратыўных умовах і пранароджаная абстракцыя спамоцваюць змену швайна доставкі.
У парадоксальнай аналізе пасля спрынту младшы інжынер з’явівае, што дадзенне адной неабязковай полькі да існуючага 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 перастае быць некальківай навантажэнням і знова стае адной з пераваг.