Dansk

En omfattende guide til TypeScript's kraftfulde Mapped Types og Conditional Types, inklusiv praktiske eksempler og avancerede anvendelser til at skabe robuste og typesikre applikationer.

Mestring af TypeScript's Mapped Types og Conditional Types

TypeScript, et supersæt af JavaScript, tilbyder kraftfulde funktioner til at skabe robuste og vedligeholdelsesvenlige applikationer. Blandt disse funktioner skiller Mapped Types og Conditional Types sig ud som essentielle værktøjer til avanceret typemanipulation. Denne guide giver en omfattende oversigt over disse koncepter, udforsker deres syntaks, praktiske anvendelser og avancerede use cases. Uanset om du er en erfaren TypeScript-udvikler eller lige er startet på din rejse, vil denne artikel udstyre dig med den viden, der skal til for at udnytte disse funktioner effektivt.

Hvad er Mapped Types?

Mapped Types giver dig mulighed for at oprette nye typer ved at transformere eksisterende. De itererer over egenskaberne i en eksisterende type og anvender en transformation på hver egenskab. Dette er især nyttigt til at skabe variationer af eksisterende typer, såsom at gøre alle egenskaber valgfri eller skrivebeskyttede.

Grundlæggende syntaks

Syntaksen for en Mapped Type er som følger:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

Praktiske eksempler

Gør egenskaber skrivebeskyttede

Lad os sige, du har en interface, der repræsenterer en brugerprofil:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

Du kan oprette en ny type, hvor alle egenskaber er skrivebeskyttede:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

Nu vil ReadOnlyUserProfile have de samme egenskaber som UserProfile, men de vil alle være skrivebeskyttede.

Gør egenskaber valgfri

På samme måde kan du gøre alle egenskaber valgfri:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfile vil have alle egenskaber fra UserProfile, men hver egenskab vil være valgfri.

Modificering af egenskabstyper

Du kan også modificere typen for hver egenskab. For eksempel kan du transformere alle egenskaber til at være strenge:

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

I dette tilfælde vil alle egenskaber i StringifiedUserProfile være af typen string.

Hvad er Conditional Types?

Conditional Types giver dig mulighed for at definere typer, der afhænger af en betingelse. De giver en måde at udtrykke type-relationer på, baseret på om en type opfylder en bestemt begrænsning. Dette ligner en ternær operator i JavaScript, men for typer.

Grundlæggende syntaks

Syntaksen for en Conditional Type er som følger:

T extends U ? X : Y

Praktiske eksempler

Afgør om en type er en streng

Lad os oprette en type, der returnerer string, hvis input-typen er en streng, og number ellers:

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

Udtrækning af type fra en Union

Du kan bruge conditional types til at udtrække en specifik type fra en union-type. For eksempel, for at udtrække ikke-nullable typer:

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

Her, hvis T er null eller undefined, bliver typen til never, som derefter filtreres fra af TypeScript's forenkling af union-typer.

Inferring af Typer

Conditional types kan også bruges til at udlede (infer) typer ved hjælp af infer nøgleordet. Dette giver dig mulighed for at udtrække en type fra en mere kompleks typestruktur.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

I dette eksempel udtrækker ReturnType returtypen fra en funktion. Den tjekker, om T er en funktion, der tager vilkårlige argumenter og returnerer en type R. Hvis den er det, returnerer den R; ellers returnerer den any.

Kombination af Mapped Types og Conditional Types

Den virkelige styrke ved Mapped Types og Conditional Types kommer fra at kombinere dem. Dette giver dig mulighed for at skabe meget fleksible og udtryksfulde type-transformationer.

Eksempel: Deep Readonly

En almindelig anvendelse er at skabe en type, der gør alle egenskaber i et objekt, inklusiv indlejrede egenskaber, skrivebeskyttede. Dette kan opnås ved hjælp af en rekursiv conditional type.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

Her anvender DeepReadonly rekursivt readonly modifikatoren på alle egenskaber og deres indlejrede egenskaber. Hvis en egenskab er et objekt, kalder den rekursivt DeepReadonly på det objekt. Ellers anvender den blot readonly modifikatoren på egenskaben.

Eksempel: Filtrering af egenskaber efter type

Lad os sige, at du vil oprette en type, der kun inkluderer egenskaber af en bestemt type. Du kan kombinere Mapped Types og Conditional Types for at opnå dette.

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

I dette eksempel itererer FilterByType over egenskaberne i T og tjekker, om typen af hver egenskab udvider U. Hvis den gør det, inkluderer den egenskaben i den resulterende type; ellers ekskluderer den den ved at mappe nøglen til never. Bemærk brugen af "as" til at remappe nøgler. Vi bruger derefter `Omit` og `keyof StringProperties` til at fjerne streng-egenskaberne fra den oprindelige interface.

Avancerede anvendelser og mønstre

Ud over de grundlæggende eksempler kan Mapped Types og Conditional Types bruges i mere avancerede scenarier til at skabe meget tilpassede og typesikre applikationer.

Distributive Conditional Types

Conditional types er distributive, når den type, der kontrolleres, er en union-type. Det betyder, at betingelsen anvendes på hvert medlem af unionen individuelt, og resultaterne kombineres derefter til en ny union-type.

type ToArray<T> = T extends any ? T[] : never;

type Result6 = ToArray<string | number>; // string[] | number[]

I dette eksempel anvendes ToArray på hvert medlem af unionen string | number individuelt, hvilket resulterer i string[] | number[]. Hvis betingelsen ikke var distributiv, ville resultatet have været (string | number)[].

Brug af Utility Types

TypeScript tilbyder adskillige indbyggede utility-typer, der udnytter Mapped Types og Conditional Types. Disse utility-typer kan bruges som byggeklodser til mere komplekse type-transformationer.

Disse utility-typer er kraftfulde værktøjer, der kan forenkle komplekse typemanipulationer. For eksempel kan du kombinere Pick og Partial for at skabe en type, der kun gør visse egenskaber valgfrie:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type OptionalDescriptionProduct = Optional<Product, "description">;

I dette eksempel har OptionalDescriptionProduct alle egenskaberne fra Product, men egenskaben description er valgfri.

Brug af Template Literal Types

Template Literal Types giver dig mulighed for at oprette typer baseret på streng-literaler. De kan bruges i kombination med Mapped Types og Conditional Types til at skabe dynamiske og udtryksfulde type-transformationer. For eksempel kan du oprette en type, der præfikser alle egenskabsnavne med en bestemt streng:

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

I dette eksempel vil PrefixedSettings have egenskaberne data_apiUrl og data_timeout.

Bedste praksis og overvejelser

Konklusion

Mapped Types og Conditional Types er kraftfulde funktioner i TypeScript, der giver dig mulighed for at skabe meget fleksible og udtryksfulde type-transformationer. Ved at mestre disse koncepter kan du forbedre typesikkerheden, vedligeholdelsen og den overordnede kvalitet af dine TypeScript-applikationer. Fra simple transformationer som at gøre egenskaber valgfrie eller skrivebeskyttede til komplekse rekursive transformationer og betinget logik, giver disse funktioner de værktøjer, du har brug for til at bygge robuste og skalerbare applikationer. Fortsæt med at udforske og eksperimentere med disse funktioner for at frigøre deres fulde potentiale og blive en dygtigere TypeScript-udvikler.

Når du fortsætter din TypeScript-rejse, så husk at udnytte den rigdom af ressourcer, der er tilgængelige, herunder den officielle TypeScript-dokumentation, online fællesskaber og open source-projekter. Omfavn kraften i Mapped Types og Conditional Types, og du vil være godt rustet til at tackle selv de mest udfordrende type-relaterede problemer.