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!