
TypeScript: Enum, union type, nebo as const?
Porovnání tří způsobů definování konstant v TypeScriptu. Kdy který použít a jaké jsou jejich výhody a nevýhody.
V TypeScriptu máte několik způsobů, jak definovat sadu konstant. Nejčastější jsou enum, union type a const assertion. Každý přístup má své výhody a nevýhody.
Přehled
| Přístup | Runtime hodnota | Tree-shaking | Iterace |
|---|---|---|---|
| enum | Ano | Ano* | Ano |
| const enum | Ne (inlinuje se) | Ano | Ne |
| Union type | Ne | - | Ne |
| as const | Ano | Ano* | Ano |
* Moderní bundlery (esbuild) dokáží hodnoty inlinovat a celý enum/objekt odstranit.
Runtime hodnota znamená, že konstanta existuje i po kompilaci do JavaScriptu — můžete ji použít pro iteraci, validaci nebo ji předat do funkce. Union type existuje jen při kompilaci a v JavaScriptu zmizí.
Tree-shaking je optimalisace, kdy build nástroj (Vite, webpack, esbuild) automaticky odstraní nepoužívaný kód z výsledného bundlu. Moderní bundlery jako esbuild dokáží enum hodnoty inlinovat a celý enum odstranit — výsledek je pak stejný jako u const enum.
Enum
TypeScript enum je speciální konstrukce, která vytváří pojmenované konstanty. Dnes je považován za legacy pattern — vznikl v době, kdy TypeScript neměl lepší alternativy. Moderní projekty preferují union types nebo as const.
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
function setStatus(status: Status) {
console.log(status);
}
setStatus(Status.Active); // 'active'
Číselný enum
Bez explicitních hodnot TypeScript přiřadí čísla od 0:
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
console.log(Direction.Up); // 0
console.log(Direction[0]); // 'Up' (reversní mapování)
Číselné enumy mají reversní mapování — můžete získat název z hodnoty. To ale zvětšuje výstupní kód.
Co se vygeneruje
Enum se kompiluje do JavaScriptového objektu:
// TypeScript
enum Status {
Active = 'active',
Inactive = 'inactive'
}
// Vygenerovaný JavaScript
var Status;
(function (Status) {
Status["Active"] = "active";
Status["Inactive"] = "inactive";
})(Status || (Status = {}));
Vygenerovaný kód používá IIFE (Immediately Invoked Function Expression) — funkci, která se definuje a okamžitě spustí. Zápis (function() { ... })() vytvoří izolovaný scope. Konstrukce Status || (Status = {}) umožňuje rozšiřovat enum napříč více soubory (pokud Status již existuje, použije se, jinak se vytvoří prázdný objekt).
Pro číselný enum je kód ještě delší kvůli reversnímu mapování.
Výhody enum
- Srozumitelná syntaxe
- Lze iterovat přes hodnoty
- Reversní mapování (pro číselné enumy)
- Hodnoty existují za běhu
Nevýhody enum
- Generuje runtime kód
- Bez bundleru generuje IIFE wrapper (moderní bundlery jako esbuild to optimalisují)
- Není nativní JavaScript — specifické pro TypeScript
- Problémy s
isolatedModules— tato volba vtsconfig.jsonzajišťuje, že každý soubor lze kompilovat samostatně (vyžadují ji nástroje jako Babel, esbuild nebo SWC). Při zapnuté volbě nelze re-exportovat enum z jiného souboru (export { Status } from './types'), protože kompilátor neví, jestli jeStatustyp nebo hodnota.
Const enum
Const enum se úplně odstraní při kompilaci — hodnoty se inlinují přímo do kódu:
const enum Status {
Active = 'active',
Inactive = 'inactive'
}
console.log(Status.Active);
// Vygenerovaný JavaScript
console.log("active"); // Enum zmizí, hodnota se vloží přímo
Omezení const enum
- Nelze iterovat — za běhu neexistuje
- Problémy při použití z jiných balíčků
- Nefunguje s
isolatedModules: true— tato volba vyžaduje, aby každý soubor byl samostatně kompilovatelný. Const enum ale potřebuje znát hodnoty z jiného souboru v době kompilace, což není možné. - Nelze použít computed hodnoty — hodnoty musí být konstantní výrazy (literály, reference na jiné členy enum). Nelze použít např.
Math.random()nebo volání funkcí.
TypeScript tým nedoporučuje const enum v knihovnách.
Union type
Union type definuje typ jako sjednocení literálů (konkrétních hodnot jako "active" nebo 200, na rozdíl od obecných typů jako string nebo number):
type Status = 'active' | 'inactive' | 'pending';
function setStatus(status: Status) {
console.log(status);
}
setStatus('active'); // OK
setStatus('unknown'); // Chyba: Argument of type '"unknown"' is not assignable
S objektovým typem
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
interface Request {
method: HttpMethod;
url: string;
}
const req: Request = {
method: 'GET',
url: '/api/users'
};
Výhody union type
- Žádný runtime kód — existuje jen v typovém systému
- Nativní TypeScript pattern
- Funguje s
isolatedModules - Výborná podpora v IDE (autocomplete)
Nevýhody union type
- Nelze iterovat — není runtime hodnota
- Nelze získat seznam hodnot za běhu
- Při mnoha hodnotách může být nepřehledné
- Při volání funkce píšete stringy přímo v kódu (
setStatus('active')) — náchylné na překlepy a IDE autocomplete funguje až po napsání uvozovky
Poslední bod řeší as const, kde místo stringů používáte pojmenované konstanty (Status.Active).
Const assertion (as const)
Const assertion vytvoří readonly objekt nebo pole s literálními typy. Nejjednodušší je pole:
const STATUSES = ['active', 'inactive', 'pending'] as const;
type Status = typeof STATUSES[number];
// Typ: "active" | "inactive" | "pending"
// Můžete iterovat
STATUSES.forEach(status => console.log(status));
Zápis [number] znamená „typ libovolného prvku pole“ — je výrazně kratší než [keyof typeof] u objektů.
S objektem (pojmenované klíče)
Pokud chcete pojmenované konstanty jako Status.Active:
const Status = {
Active: 'active',
Inactive: 'inactive',
Pending: 'pending'
} as const;
type StatusType = typeof Status[keyof typeof Status];
// Typ: "active" | "inactive" | "pending"
setStatus(Status.Active); // Pojmenovaná konstanta
setStatus('active'); // Také funguje
Zápis typeof Status[keyof typeof Status] je krkolomný, ale dá se zjednodušit pomocí helper typu.
Co se vygeneruje
// TypeScript
const Status = {
Active: 'active',
Inactive: 'inactive'
} as const;
// Vygenerovaný JavaScript
const Status = {
Active: 'active',
Inactive: 'inactive'
};
Konstanta as const se zkompiluje do běžného objektu — žádný overhead.
Výhody as const
- Kombinuje výhody enum a union type
- Runtime hodnoty existují
- Lze iterovat
- Minimální vygenerovaný kód
- Bez bundleru generuje menší kód než enum
- Funguje s
isolatedModules - Nativní JavaScript pattern
Nevýhody as const
- Více kódu pro extrakci typu (
typeof X[keyof typeof X]) - Méně intuitivní pro začátečníky
- Stále můžete psát stringy přímo v kódu —
if (status === "active")projde, i když máteStatus.Active
Poslední bod lze řešit ESLint pravidlem zakazujícím stringové literály tam, kde existuje pojmenovaná konstanta. TypeScript sám překlepy zachytí (typ nedovolí "actve"), ale nenutí vás používat Status.Active místo "active". U union types toto řešit nelze — tam pojmenované konstanty neexistují a stringy jsou jediná možnost.
Helper pro as const
Pro jednodušší práci s as const můžete vytvořit helper:
// Helper pro získání union typu z objektu
type ValueOf<T> = T[keyof T];
const Status = {
Active: 'active',
Inactive: 'inactive',
Pending: 'pending'
} as const;
type Status = ValueOf<typeof Status>;
// "active" | "inactive" | "pending"
// Helper pro pole
type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = ArrayElement<typeof ROLES>;
// "admin" | "user" | "guest"
Praktické porovnání
Definice
// Enum
enum ColorEnum {
Red = 'red',
Green = 'green',
Blue = 'blue'
}
// Union type
type ColorUnion = 'red' | 'green' | 'blue';
// As const
const Color = {
Red: 'red',
Green: 'green',
Blue: 'blue'
} as const;
type ColorConst = typeof Color[keyof typeof Color];
Použití
// Enum
function paintEnum(color: ColorEnum) {}
paintEnum(ColorEnum.Red);
// Union type
function paintUnion(color: ColorUnion) {}
paintUnion('red');
// As const
function paintConst(color: ColorConst) {}
paintConst(Color.Red);
paintConst('red'); // Také funguje
Iterace
// Enum — pozor u číselných enumů (vrací i klíče)
Object.values(ColorEnum).forEach(color => console.log(color));
// Union type — nelze
// ColorUnion.forEach... // Chyba - typ neexistuje za běhu
// As const — funguje
Object.values(Color).forEach(color => console.log(color));
Kdy co použít
Použijte union type když:
- Nepotřebujete runtime hodnoty
- Máte jednoduchý seznam možností
- Chcete co nejmenší bundle
type Size = 'sm' | 'md' | 'lg' | 'xl';
type Theme = 'light' | 'dark';
type HttpStatus = 200 | 201 | 400 | 404 | 500;
Použijte as const když:
- Potřebujete runtime hodnoty i typy
- Chcete iterovat přes hodnoty
- Vytváříte knihovnu
- Máte
isolatedModules: true
const API_ENDPOINTS = {
Users: '/api/users',
Posts: '/api/posts',
Comments: '/api/comments'
} as const;
type Endpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS];
// Můžete iterovat i typovat
Object.entries(API_ENDPOINTS).forEach(([name, url]) => {
console.log(`${name}: ${url}`);
});
Použijte enum když:
- Pracujete s existujícím kódem, který enumy používá
- Potřebujete reversní mapování (číselné enumy)
- Tým je na enumy zvyklý
Vyhněte se const enum
Const enum má příliš mnoho problémů. Použijte raději as const.
Problém s porovnáváním stringů
Častý problém v kódu je použití stringů přímo při porovnávání:
// "Magic string" - string přímo v kódu
if (status === "active") {
// ...
}
// Lepší - pojmenovaná konstanta
if (status === Status.Active) {
// ...
}
Stringy přímo v kódu (tzv. magic strings) jsou problematické:
- Náchylné na překlepy
- Těžší refaktoring — při přejmenování musíte hledat všechny výskyty
- Žádný autocomplete při psaní
Co TypeScript zachytí
Pokud máte správně definovaný typ, TypeScript zachytí překlepy:
type Status = 'active' | 'inactive';
function check(status: Status) {
if (status === "actve") { } // ✗ Chyba - překlep
if (status === "unknown") { } // ✗ Chyba - není v typu
if (status === "active") { } // ✓ OK
}
Co TypeScript nezachytí
TypeScript nevynucuje použití pojmenovaných konstant. I když máte Status.Active, můžete stále psát string přímo:
const Status = { Active: 'active', Inactive: 'inactive' } as const;
type StatusType = typeof Status[keyof typeof Status];
function check(status: StatusType) {
if (status === Status.Active) { } // ✓ Pojmenovaná konstanta
if (status === "active") { } // ✓ Také projde - TypeScript neprotestuje
}
Jak to řeší jednotlivé přístupy
| Přístup | Zachytí překlepy | Vynucuje konstanty |
|---|---|---|
| enum | ✓ | ✓ — "active" neprojde |
| union type | ✓ | ✗ — stringy jsou jediná možnost |
| as const | ✓ | ✗ — stringy i konstanty projdou |
Pokud potřebujete striktně vynucovat použití konstant, enum je jediná možnost. Pro většinu projektů ale stačí, že TypeScript zachytí překlepy.
Migrace z enum na as const
Pokud chcete migrovat existující enum:
// Původní enum
enum OldStatus {
Active = 'active',
Inactive = 'inactive'
}
// Nový as const
const Status = {
Active: 'active',
Inactive: 'inactive'
} as const;
type Status = typeof Status[keyof typeof Status];
// Použití zůstává téměř stejné
// OldStatus.Active → Status.Active
Ideální řešení neexistuje
Každý přístup má kompromisy:
| Požadavek | enum | union | as const |
|---|---|---|---|
| Runtime hodnoty | ✓ | ✗ | ✓ |
| Pojmenované konstanty | ✓ | ✗ | ✓ |
| Vynucení konstant (ne stringů) | ✓ | ✗ | ✗ |
| Žádný runtime overhead | ✗ | ✓ | ✓ |
| Funguje s isolatedModules | ⚠️ | ✓ | ✓ |
| Jednoduchý zápis | ✓ | ✓ | ✗ |
Nejbližší k ideálu je as const — má většinu výhod. Jediná skutečná nevýhoda je, že nevynucuje použití pojmenovaných konstant místo stringů. Pro většinu projektů to není problém, protože TypeScript zachytí překlepy.
Závěr
- Union type — pro jednoduché případy bez runtime hodnot
- As const — pro většinu případů s runtime hodnotami
- Enum — pouze pokud máte dobrý důvod
- Const enum — vyhněte se
Moderní TypeScript projekty preferují as const nebo union types. Enumy jsou stále validní volba, ale přinášejí zbytečný runtime overhead.