Moderní tvorba webových aplikací
O webu

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.

13 minut

V TypeScriptu máte několik způsobů, jak definovat sadu konstant. Nejčastější jsou enum, union typeconst 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 v tsconfig.json zajišť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 je Status typ 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áte Status.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

  1. Union type — pro jednoduché případy bez runtime hodnot
  2. As const — pro většinu případů s runtime hodnotami
  3. Enum — pouze pokud máte dobrý důvod
  4. 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.

Související články

Jak vkládat 3D objekty na web pomocí Three.js

Které formáty použít, jak vytvářet modely pomocí AI a kdy raději použít obrázek nebo video.

15 minut

Detekce otevření DevTools

Jak zjistit, že se na stránce otevřely vývojářské nástroje.

13 minut

JavaScript nullundefined

Rozdíly mezi nullundefined v JavaScriptu, kdy je používat a jak se vyhnout běžným chybám.

12 minut

Sleep v JavaScriptu

Jak implementovat sleep/delay funkcionalitu v JavaScriptu pomocí Promiseasync/await

6 minut

Novinky e-mailem

Když budu mít něco opravdu zajímavého, můžu vám to poslat e-mailem

Přidej se k 500+ čtenářům
Jen kvalitní obsah
Žádný spam

Web jecas.cz píše Bohumil Jahoda, kontakt
Seznam všech článků
2013–2026