Poprzednio poznaliśmy już pierwszy własny typ rekurencyjny w TypeScript (FlattenNested), dzisiaj napiszemy sobie pierwszy mapowany rekurencyjny typ. Do dzieła!

Ok, najpierw przypomnijmy sobie Flatten i infer:

const MyArray = [
    { name: "Alice", age: 15 },
    { name: "Bob", age: 23 },
    { name: "Eve", age: 38 },
  ];

const AnotherArray = [1,2,3,4];

type Flatten<T> = T extends Array<infer NestedType> ? NestedType : T;

type Person444 = Flatten<typeof MyArray>

// type Person444 = {
//     name: string;
//     age: number;
// }

type Type444 = Flatten<typeof AnotherArray>

// type Type444 = number

Mam nadzieję, że pamiętamy. Teraz FlattenNested, czyli nasz rekurencyjny typ z wcześniejszej lekcji:

type FlattenNested<T> = T extends Array<infer Nested> ? FlattenNested<Nested> : T;

const nestedArr = [ [[1],[2],[3]], [[4],[5],[6]]];

type nestedArrType = typeof nestedArr;

//type nestedArrType = number[][][]

type nestedArrFlatType = FlattenNested<nestedArrType>

//type nestedArrFlatType = number

Proste, a dzisiaj nawet infer nie będzie. Będzie za to mapowanie, mapped type, mam nadzieję, że pamiętamy. Ok, taki typ napiszemy:

type User999 = {
    firstName: string;
    lastName: string;
    age: number;
    isMarried: boolean;
    birthDate: Date;
    timestamps: {
        createdAt: Date;
        updatedAt: Date;
        deletedAt: Date;
    }
};

Chcemy zamienić daty na string. To będziemy mapować i sprawdzać, czy data rozszerza typ property, jak tak, to przypisujemy string, jak nie, to przypisujemy property-typ[property]:

type DateReplacer<T extends object> = {
    [P in keyof T]: Date extends T[P] ? string: T[P];
};

Wiem, że może być ciężko to zrozumieć, ale jak rozłożymy to sobie na drobne kroczki w głowie to wszystko się ułoży. Tak to wygląda:

type User999 = {
    firstName: string;
    lastName: string;
    age: number;
    isMarried: boolean;
    birthDate: Date;
    timestamps: {
        createdAt: Date;
        updatedAt: Date;
        deletedAt: Date;
    }
};

type DateReplacer<T extends object> = {
    [P in keyof T]: Date extends T[P] ? string: T[P];
};

type User999DateReplaced = DateReplacer<User999>;

// type User999DateReplaced = {
//     firstName: string;
//     lastName: string;
//     age: number;
//     isMarried: boolean;
//     birthDate: string;
//     timestamps: {
//         createdAt: Date;
//         updatedAt: Date;
//         deletedAt: Date;
//     };
// }

Jak widać, brakuje rekurencji. Tu akurat musimy zdjąć object z constrainta, aby rekurencyjne wywołanie zadziałało:

type DateReplacerRec<T> = {
    [P in keyof T]: Date extends T[P] ? string: DateReplacerRec<T[P]>;
};

Tak już zupełnie mimochodem, mam nadzieję, że pamiętamy różnicę między extends object a extends {}. Extends object – jest obiektem w klasycznym tego rozumieniu. Extends {} – nie jest nullem ani undefined.

Ok, zobaczmy jak to działa:

type DateReplacerRec<T> = {
    [P in keyof T]: Date extends T[P] ? string: DateReplacerRec<T[P]>;
};

let obj999: DateReplacerRec<User999> = {
    firstName: "a",
    lastName: "b",
    age: 30,
    isMarried: false,
    birthDate: "0",
    timestamps: {
        createdAt: "0",
        updatedAt: "0",
        deletedAt: "0"
    }
} 

Swoją drogą, jak byśmy chcieli ten constraint utrzymać, to możemy pomieszać ze sobą DateReplacerRec i date replacer…

Nie, ok , wystarczy odpowiedni guard, aby nie wywoływać się rekurencyjnie na czymś, co może nie być obiektem:

type DateReplacerRec<T extends object> = {
    [P in keyof T]: Date extends  T[P]  ? string: T[P] extends object ?  DateReplacerRec<T[P]> : T[P];
};

let obj999: DateReplacerRec<User999> = {
    firstName: "a",
    lastName: "b",
    age: 30,
    isMarried: false,
    birthDate: "0",
    timestamps: {
        createdAt: "0",
        updatedAt: "0",
        deletedAt: "0"
    }
} 

Czyli taka logika:

  • Bierz typ T, który musi być obiektem
  • Przeiteruj po P w keyof T (keyof T czyli lista kluczy T)
  • Sprawdź, czy Data rozszerza T[P] (typ przypisany do klucza P w T)
  • Jeżeli tak, zamień na string
  • Sprawdź, czy T[p] rozszerza obiekt, jeżeli tak, to rekurencyjnie się na nim wywołaj
  • Jeżeli T[p] nie jest ani datą, ani obiektem, to jest po prostu jakimś typem, który ma być przypisany do property w nowym, zmapowanym obiekcie

Więcej TSa niedługo!