Interface vs. Type: Proč jsem vytvořil vlastní pravidlo ESLint (a jak to změnilo náš tým)
Jako front-end vývojář s více než šesti lety zkušeností jsem zažil už pěknou řádku debat o TypeScriptu. Jedna, která vždy vyvolá vášnivé diskuze, je klasické dilema „Interface vs. Type“. Na začátku své kariéry jsem o tom moc nepřemýšlel – zdálo se, že obojí splní svůj účel. Ale jak projekty rostly a týmy se rozšiřovaly, uvědomil jsem si, že malá rozhodnutí jako toto mohou rozhodnout o konzistenci kódu (nebo ji rozbít). To je důvod, proč jsem nakonec vytvořil vlastní pravidlo ESLint, abych vynutil používání type aliasů namísto deklarací interface v naší TypeScript codebase. V tomto článku se podělím o to, proč jsem se tak rozhodl, jak jsem pravidlo vytvořil a jak to proměnilo naše pracovní postupy. Navíc přihodím pár tipů, abyste si to mohli vyzkoušet sami.
Chaos při škálování týmu bez jasných pravidel
Když jsem ve své firmě začal pracovat s TypeScriptem, náš tým byl malý – jen hrstka vývojářů. Měli jsme ústní dohodu, že se budeme držet type aliasů místo deklarací interface, protože nám připadaly předvídatelnější (více o tom později). Zpočátku to fungovalo skvěle. Byli jsme sladění, náš kód byl čistý a život byl fajn. Pak ale náš tým rychle vyrostl. Během roku jsme měli 10 vývojářů přispívajících do stejné codebase, a tehdy se věci zkomplikovaly.
Noví členové týmu si nebyli vždy vědomi našeho „nepsaného pravidla“. Někteří preferovali interface, protože jim to připadalo povědomější z jejich Java backgroundu. Jiní míchali oba přístupy, aniž by si toho vůbec všimli. Code reviews se změnily v nekonečné debaty o stylu spíše než o podstatě. Bylo jasné, že potřebujeme lepší způsob, jak vynutit konzistenci – něco automatizovaného, spolehlivého a škálovatelného. Takže jsem se rozhodl vyhrnout si rukávy a vytvořit vlastní ESLint plugin: eslint-plugin-interface-to-type. Ten by automaticky vynucoval použití type namísto interface napříč celou naší codebase a zachránil nás před námi samými.
Proč jsem si vybral type místo interface
Než se ponoříme do technických detailů, pojďme si říct, proč jsem si vůbec vybral type místo interface. Na první pohled se mohou zdát zaměnitelné – oba definují tvary objektů, že? Ale je tu jemný rozdíl, který vás může ve větších projektech kousnout: Declaration Merging (slučování deklarací).
V TypeScriptu platí, že pokud deklarujete dvě definice interface se stejným názvem, automaticky se sloučí do jedné. Zde je rychlý příklad:
interface User {
id: number;
}
interface User {
name: string;
}
// TypeScript merges them into:
interface User {
id: number;
name: string;
}
Toto chování může být v některých případech užitečné, například při rozšiřování typů knihoven třetích stran. Ale v prostředí spolupráce je to recept na zmatek. Představte si dva vývojáře, kteří nevědomky deklarují konfliktní vlastnosti v oddělených souborech – je to chyba čekající na to, až se stane. Na druhou stranu, type aliasy se neslučují. Pokud se pokusíte deklarovat dva type aliasy se stejným názvem, TypeScript vyhodí chybu kompilace, což vás donutí vyřešit konflikt předem. Pro mě je tato předvídatelnost záchranou života.
Kromě slučování deklarací jsou type aliasy flexibilnější. Mohou reprezentovat uniony, průniky (intersections) a primitiva – věci, se kterými interface zápasí. I když interface má své místo (například když potřebujete rozšiřovat třídy), zjistil jsem, že type je bezpečnější výchozí volbou pro většinu našich případů použití.
Tvorba pravidla ESLint: Od chaosu ke konzistenci
Teď, když jsem měl jasný důvod pro vynucení type, potřeboval jsem nástroj, který by to zajistil automaticky. Tady přichází na řadu můj ESLint plugin, eslint-plugin-interface-to-type. Chtěl jsem pravidlo, které by označilo jakoukoli deklaraci interface, navrhlo její nahrazení za type alias a dokonce poskytlo možnost automatické opravy (autofix) pro zefektivnění procesu.
Tvorba vlastního pravidla ESLint byla zpočátku trochu děsivá – nikdy předtím jsem žádné nepsal. Ale po nějakém průzkumu jsem tomu přišel na kloub. Plugin využívá AST (Abstract Syntax Tree) ESLintu k detekci deklarací interface a jejich transformaci na ekvivalentní type aliasy. Například toto:
interface User {
id: number;
name: string;
role: 'admin' | 'user';
}
se označí a lze to automaticky opravit na:
type User = {
id: number;
name: string;
role: 'admin' | 'user';
};
Nejtěžší částí bylo řešení okrajových případů – jako jsou deklarace interface, které rozšiřují (extend) jiné interfacy nebo mají složité generiky. Strávil jsem hodiny laděním pravidla, abych zajistil kompatibilitu a vyhnul se falešným pozitivům. Další výzvou byla implementace funkce automatické opravy. Možnost --fix v ESLintu vyžaduje poskytnutí přesných transformací, takže jsem musel pečlivě mapovat syntaxi interface na syntaxi type, aniž bych rozbil kód.
Jakmile byl plugin hotový, jeho nastavení bylo přímočaré. Stačí jej nainstalovat jako dev dependency:
npm install eslint-plugin-interface-to-type --save-dev
Poté jej přidejte do své konfigurace ESLint:
{
"plugins": ["interface-to-type"],
"rules": {
"interface-to-type/prefer-type-over-interface": "error"
}
}
Spusťte ESLint s --fix a on automaticky převede vaše deklarace interface na type aliasy. Hotovo!
Dopad na naše pracovní postupy: Méně bojování, více stavění
Dopad tohoto pravidla na náš tým byl okamžitý. Dříve jsme trávili kusy code reviews dohadováním se o interface vs. type. Nyní za nás pravidlo vynucuje konzistenci a uvolňuje mentální prostor pro důležitější diskuze – jako je architektura nebo optimalizace výkonu.
Také nám to ušetřilo čas. Noví vývojáři už nepotřebovali dlouhou úvodní přednášku o našich preferencích stylu. Pravidlo to prostě vyřešilo. Code reviews se zrychlily, protože jsme už nehledali hnidy v syntaxi. Je to malá změna, ale dominový efekt byl obrovský.
Tipy pro vývojáře: Jak zařídit, aby to fungovalo pro váš tým
Pokud vás tato myšlenka zaujala, zde je několik tipů, které vám pomohou vynutit používání type – nebo jakéhokoli standardu kódování – ve vašich projektech:
- Nejprve pochopte své potřeby: Než začnete vynucovat type namísto interface, ujistěte se, že to odpovídá cílům vašeho týmu. Pokud se silně spoléháte na Declaration Merging nebo implementace tříd, interface může mít stále své místo. Pro nás type fungoval lépe v 99 % případů, takže dávalo smysl se na něm sjednotit.
- Automatizujte pomocí nástrojů jako ESLint: Nespoléhejte na ústní dohody – ty se neškálují. Nástroje jako ESLint (nebo Prettier pro formátování) mohou konzistentně vynucovat pravidla v celém týmu. Psaní vlastního pravidla může znít zastrašujícím dojmem, ale je to skvělá zkušenost k učení a z dlouhodobého hlediska se vyplatí.
- Podporujte zapojení týmu (Buy-In): Pokud zavádíte nový standard, získejte nejprve zpětnou vazbu od kolegů. Před zavedením jsem své pravidlo ESLint sdílel s týmem a jejich vstupy mi pomohly zachytit několik okrajových případů, které jsem přehlédl.
Vyzkoušejte to sami a dejte mi vědět!
Přechod na type aliasy a jejich vynucení pomocí vlastního pravidla ESLint byl pro můj tým zásadní změnou. Náš kód se stal předvídatelnějším, recenze produktivnějšími a onboarding hladším. Pokud řešíte podobné problémy – nebo jen chcete experimentovat s přísnějšími standardy – doporučuji vám to vyzkoušet.
Začněte instalací eslint-plugin-interface-to-type a jeho spuštěním na vaší codebase. Uvidíte, jaký je to pocit mít konzistentní přístup k typům v TypeScriptu. Máte jiný pohled na interface vs. type? Nebo pravidlo odhalilo ve vašem projektu nějaké nečekané problémy?