Jeg var ute på min vanlige joggetur da jeg plutselig stoppet opp. Der, i et vindu, så jeg noe av det vakreste jeg hadde sett på lenge: LEGO-blomster. Fargerike og evigvarende. Som blomsterentusiast føltes det som å oppdage en perfekt kombinasjon av to verdener.
Det er fascinerende hvor tidløst populært LEGO er. En av styrkene ved LEGO er hvor enkelt det er å identifisere hver brikke, selv i en stor samling. Bare ved et raskt blikk kan du skille en 2x4-kloss fra en blomsterstengel eller et hjul. Dette prinsippet - å kunne identifisere typen basert på tydelige kjennetegn - er akkurat det som gjør Discriminated Unions så kraftfulle i programmering.
På samme måte som du umiddelbart gjenkjenner en LEGO-brikke basert på dens karakteristikker, kan TypeScript identifisere objekttyper gjennom en diskriminator-egenskap, og Zod kan validere at data følger disse mønstrene under kjøretid. I denne bloggposten skal vi se på et praktisk eksempel på hvordan du kan bruke disse verktøyene for å bygge robuste og typesikre systemer.

¶Hva er Discriminated Unions?
Discriminated Unions lar oss skape en type med flere varianter, men med én felles egenskap. Dette er den såkalte “diskriminatoren” som brukes for å bestemme hvilken spesifikk type vi jobber med. Under lager vi en type for LEGO-brikker hvor legoType
er diskriminator-egenskapen.
type LEGOBrikke =
| { legoType: 'kloss'; antallKnotter: number, farge: string }
| { legoType: 'blomsterblad'; farge: string; bredde: number }
| { legoType: 'stengel'; farge: string; lengde: number }
| { legoType: 'hjul'; diameter: number; farge: string };

¶Spillereglene: TypeScript vs. Zod
Discriminated Unions er et kraftig mønster som støttes av både TypeScript og Zod, men på litt forskjellige måter og med ulike formål. For å forstå hvordan vi kan utnytte dette mønsteret best mulig, må vi først forstå hvilken rolle hver av disse teknologiene spiller:
TypeScript
- Statisk typekontroll under kompilering
- Typene eksisterer kun under utvikling, ikke i produksjonskoden
- Gir IDE-støtte som autofullføring og feilmarkering
- Fanger opp typefeil tidlig i utviklingsprosessen
Zod
- Validerer data under kjøretid
- Særlig nyttig for å validere ekstern data (API-responser, brukerinput)
- Kaster feil hvis data ikke følger det definerte skjemaet
- Integreres sømløst med populære biblioteker som React Hook Form og TanStack Form
Hvordan de jobber sammen: Du definerer datastrukturen én gang i Zod, og TypeScript kan utlede (infer) typene fra Zod-skjemaene. Dette samarbeidet eliminerer duplisering og sikrer at typene og valideringen er synkronisert. I eksemplene som følger skal vi se hvordan dette fungerer i praksis.
¶Discriminated Unions i praksis: Et LEGO-samlingssystem
Heldigvis fant jeg lego-blomstene på nett, så det er fare for at jeg nå blir en del av AFOL – også kjent som “Adult fans of LEGO”. Som ny LEGO-entusiast trenger jeg selvfølgelig et system for å holde orden på samlingen min. Dette gir meg den perfekte anledningen til å demonstrere Discriminated Unions med TypeScript og Zod.


Steg 1: Velge diskriminator
Det første og viktigste valget er diskriminatoren - den egenskapen som gjør at vi kan skille mellom ulike typer. I vårt LEGO-samlingssystem ønsker jeg å skille på sett-typer siden det tydelig forteller hvilken kategori av LEGO vi jobber med.
import { z } from "zod";
// Definere LEGO-sett typer med enum for bruk som diskriminator
const legoSetTypeSchema = z.enum([
"PLANTS",
"VEHICLES",
"BUILDINGS"
]);
// Type-inferens for LEGO-sett typer
export type LegoSetType = z.infer<typeof legoSetTypeSchema>;
export const LegoSetEnum = legoSetTypeSchema.enum;
Vi bruker en enum her fordi den gir oss et avgrenset sett med forhåndsdefinerte verdier. Dette er perfekt for en diskriminator siden det gir oss uttømmende type-sjekking og forhindrer ugyldige verdier.
Steg 2: Definere basis for LEGO-sett
Nå definerer vi de grunnleggende egenskapene som alle LEGO-sett deler, uavhengig av type:
const baseLegoSetSchema = z.object({
setNumber: z.string(),
name: z.string(),
pieceCount: z.number().positive(),
ageGroup: z.string(),
price: z.number().positive(),
buildInstructions: z.boolean().default(true),
});
export type BaseLegoSetType = z.infer<typeof baseLegoSetSchema>;
Steg 3: Definere spesifikke kategoriskjemaer
Vi bygger på basisskjemaet og definerer hver kategori i mer detalj. Som du ser under kan hver kategori ha veldig forskjellige egenskaper. Legg merke til hvordan vi bruker LegoSetEnum
som ble definert i steg 1.
// Plantesett
const plantSetSchema = baseLegoSetSchema.extend({
type: z.literal(LegoSetEnum.PLANTS),
details: z.object({
plantType: z.enum(["rose", "sunflower", "orchid", "cactus"]),
height: z.number().positive(),
vaseIncluded: z.boolean().default(true),
}),
});
// Kjøretøysett
const vehicleSetSchema = baseLegoSetSchema.extend({
type: z.literal(LegoSetEnum.VEHICLES),
details: z.object({
vehicleType: z.enum(["car", "boat", "plane", "train"]),
brand: z.string().optional(),
model: z.string().optional(),
}),
});
// Bygningssett
const buildingSetSchema = baseLegoSetSchema.extend({
type: z.literal(LegoSetEnum.BUILDINGS),
details: z.object({
buildingType: z.enum(["residential", "historical", "fantasy"]),
floors: z.number().positive(),
furnished: z.boolean(),
}),
});
Steg 4: Kombinere til en Discriminated Union
Her bruker vi type
som diskriminator for å kombinere alle skjemaene:
// LEGO-sett union
const legoSetSchema = z.discriminatedUnion("type", [
plantSetSchema,
vehicleSetSchema,
buildingSetSchema,
]);
// Type-inferens for unionen fra skjemaet
type LegoSet = z.infer<typeof legoSetSchema>;
Steg 5: Skape og bruke typesikker hjelpefunksjon
Nå som vi har definert strukturen, trenger vi en enkel måte å opprette nye LEGO-sett på. Uten en hjelpefunksjon måtte vi manuelt skrevet ut alle detaljene for hvert sett, noe som fort blir omstendelig og feilbetont:
// Manuell oppretting uten hjelpefunksjon
const roseSetManual: LegoSet = {
type: LegoSetEnum.PLANTS, // Må huske å sette riktig type
setNumber: "10280",
name: "Flower Bouquet",
pieceCount: 756,
ageGroup: "18+",
price: 499,
details: {
plantType: "rose",
height: 25,
vaseIncluded: true,
}
};
Dette virker enkelt nok for ett eksempel, men med flere titalls eller hundretalls sett blir det fort tungt å vedlikeholde. Dessuten er det lett å gjøre feil når man manuelt må sørge for at type
og details
samsvarer.
En bedre løsning er å lage en hjelpefunksjon:
// Hjelpefunksjon for å lage LEGO-sett objekter
export function createLegoSet<T extends LegoSet>(
setType: LegoSetType,
details: T["details"],
baseInfo: Omit<BaseLegoSetType, "type" | "details">
): LegoSet {
return {
...baseInfo,
type: setType,
details,
} as LegoSet;
}
Denne funksjonen gir flere fordeler:
- Den sikrer at
type
ogdetails
alltid samsvarer - TypeScript kan gi bedre type-sjekking når vi oppretter nye sett
- Vi kan fokusere på å oppgi data snarere enn å bekymre oss for strukturen
- Det blir enklere å endre strukturen senere hvis nødvendig
Med denne hjelpefunksjonen på plass kan vi nå enkelt opprette ulike typer LEGO-sett mens TypeScript sikrer at vi oppgir korrekte detaljer for hver type:
// Opprette et plantesett
const roseSet = createLegoSet(
LegoSetEnum.PLANTS,
{
plantType: "rose",
height: 25,
vaseIncluded: true,
},
{
setNumber: "10280",
name: "Flower Bouquet",
pieceCount: 756,
ageGroup: "18+",
price: 499,
}
);
// Opprette et kjøretøysett
const carSet = createLegoSet(
LegoSetEnum.VEHICLES,
{
vehicleType: "car",
brand: "Porsche",
model: "911",
},
{
setNumber: "10295",
name: "Porsche 911",
pieceCount: 1458,
ageGroup: "18+",
price: 1499,
}
);
IDE-en vil nå hjelpe deg med å velge riktige egenskaper for hver type. Hvis du prøver å gi plantedetaljer til et kjøretøy, vil TypeScript gi en feilmelding før koden kompileres. Deilig, ikke sant?

Type innsnevring i handling (hvordan TypeScript bruker diskriminatoren)
Når vi bruker union-typen i en funksjon, kan TypeScript automatisk snevre inn de tilgjengelige egenskapene basert på verdien av diskriminatoren. Under ser du at IDE-en forstår konteksten i hver gren av switch-uttrykket og gir deg presise forslag. I BUILDINGS-casen vet IDE-en at buildingType
og floors
er tilgjengelige, men ikke egenskaper fra andre typer som vehicleType
eller height
.

Validering av ekstern data (hvordan Zod validerer under kjøretid)
Med skjemaet på plass kan vi nå validere data når programmet kjører:
// Eksempel på data som kan komme fra en API
const inputData = {
type: "PLANTS",
setNumber: "10281",
name: "Bonsai Tree",
pieceCount: 878,
ageGroup: "18+",
price: 599,
details: {
plantType: "bonsai", // Dette vil feile validering!
height: 20,
vaseIncluded: true
}
};
// Validering med Zod
try {
const validatedSet = legoSetSchema.parse(inputData);
beskrivLEGOSett(validatedSet);
} catch (error) {
console.error("Invalid LEGO set:", error);
}
Error fra validering:
ZodError: [
{ "code": "invalid_enum_value",
"path": ["details", "plantType"],
"message": "Invalid enum value. Expected 'rose' | 'sunflower' | 'orchid' | 'cactus'"
}
]
¶Zod 4 går fra beta til stabil 19. mai 2025
Zod 4 blir stabil senere i mai etter en lengre betaperiode. En av forbedringene er at Zod nå automatisk identifiserer diskriminatoren i Discriminated Unions. Dette gjør koden ryddigere og mer intuitiv. Hvis ingen felles diskriminatornøkkel finnes, kaster Zod en feil allerede ved skjemainitialisering.
// I Zod 4 trenger du ikke spesifisere diskriminatornøkkelen
const legoSetSchema = z.discriminatedUnion([
plantSetSchema,
vehicleSetSchema,
buildingSetSchema
]);
Andre nyttige forbedringer inkluderer:
- Sammensettbare unioner: Du kan nå bruke en discriminated union som medlem av en annen union
- Betydelige ytelsesforbedriger: Validering er 3-7x raskere, TypeScript-kompilering er dramatisk forbedret, og pakkestørrelsen er redusert med 50%
¶Fem grunner til å digge Discriminated Unions med TypeScript og Zod
-
Én definisjon, to fordeler: Definér Zod-skjemaet én gang, få både validering og TypeScript-typer.
-
Typesikkerhet: TypeScript kan snevre inn typen basert på diskriminator-egenskapen, som gir full IDE-støtte.
-
Validering i kjøretid: Zod sikrer at dataene dine faktisk har riktig struktur når programmet kjører, ikke bare under utvikling.
-
Uttømmelseskontroll: Kompilatoren sørger for at du har håndtert alle mulige tilfeller i switch-uttrykk og lignende.
-
Ryddig kodeorganisering: Hver variant av unionen har sin egen struktur og valideringsregler.
Det var alt jeg hadde å si for denne gangen, nå skal jeg nyte synet av mine nye LEGO-blomster. Hva er din erfaring rundt Discriminated Unions? Jeg hører gjerne tilbakemelding eller refleksjoner!
