mapped types în TypeScript

Construiesc un tip nou trecând prin fiecare cheie a altui tip: { [K in keyof T]: ... }.

Sintaxă
type Nou<T> = { [K in keyof T]: TipNou };
type Opt<T> = { [K in keyof T]?: T[K] };
type Fara<T> = { -readonly [K in keyof T]-?: T[K] };
type Redenumit<T> = { [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K] };

Ce face mapped types

Un mapped type creează un tip obiect parcurgând o listă de chei: { [K in Chei]: Tip }. Cel mai des cheile sunt keyof T, deci rezultatul are aceleași proprietăți ca T, dar cu alt tip sau alți modificatori. Așa sunt construite Partial, Required, Readonly, Pick și Record.

Modificatorii ? și readonly se pot adăuga (+?, +readonly sau doar ?) sau scoate (-?, -readonly). În interior, T[K] este tipul proprietății curente.

Cu as (TypeScript 4.1) poți redenumi cheile, de exemplu cu un template literal (`get${Capitalize<K>}`), sau le poți elimina: o cheie transformată în never dispare din rezultat.

Exemple

Exemplul 1: Toate proprietățile devin text pentru un formular

Câmpurile unui formular HTML sunt mereu text, indiferent de tipul datelor.

interface Elev {
    nume: string;
    varsta: number;
    bursier: boolean;
}
type Formular<T> = { [K in keyof T]: string };
type Erori<T> = { [K in keyof T]?: string };

function valideaza(f: Formular<Elev>): Erori<Elev> {
    const erori: Erori<Elev> = {};
    if (f.nume.trim() === "") erori.nume = "Numele este obligatoriu";
    if (!/^\d+$/.test(f.varsta)) erori.varsta = "Vârsta trebuie să fie un număr";
    return erori;
}

console.log(valideaza({ nume: "Ana", varsta: "19", bursier: "da" }));
console.log(valideaza({ nume: " ", varsta: "nouăsprezece", bursier: "nu" }));

Rezultat

{}
{
  nume: 'Numele este obligatoriu',
  varsta: 'Vârsta trebuie să fie un număr'
}

Exemplul 2: Getter-e generate cu as

Fiecare cheie K devine getK cu prima literă mare, iar valoarea este o funcție.

type Getteri<T> = {
    [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};

interface Oras {
    nume: string;
    populatie: number;
}

function cuGetteri(o: Oras): Getteri<Oras> {
    return { getNume: () => o.nume, getPopulatie: () => o.populatie };
}

const iasi = cuGetteri({ nume: "Iași", populatie: 271692 });
console.log(iasi.getNume(), iasi.getPopulatie());

Rezultat

Iași 271692

Exemplul 3: Scoaterea lui readonly și a lui ?

interface Setari {
    readonly limba?: string;
    readonly tema?: string;
}
type Editabil<T> = { -readonly [K in keyof T]-?: T[K] };

const s: Editabil<Setari> = { limba: "ro", tema: "întunecată" };
s.tema = "luminoasă";      // permis: nu mai este readonly
console.log(s);

Rezultat

{ limba: 'ro', tema: 'luminoasă' }

Greșeli frecvente

  • Uiți & string când folosești cheia într-un template literal: keyof T poate conține și number sau symbol, iar Capitalize acceptă doar text.
  • Scrii mapped type într-o interface. Sintaxa [K in ...] funcționează doar în aliasuri type.

Comenzi înrudite

  • keyof: keyof T dă union-ul numelor de proprietăți ale lui T, de exemplu „nume” | „oras”.
  • indexed access (T[K]): T[„cheie”] dă tipul unei proprietăți din T; T[number] dă tipul elementelor unui tablou.
  • Partial: Partial<T> face toate proprietățile lui T opționale; util pentru actualizări în care trimiți doar ce se schimbă.
  • Record: Record<K, T> descrie un obiect ale cărui chei sunt K și ale cărui valori au toate tipul T.
  • template literal types: Tipuri de text construite ca un template string: `id-${number}` acceptă „id-1”, „id-42” și așa mai departe.
  • conditional types: Un tip care alege între două variante după o condiție: T extends U ? X : Y.