Nie wiem, czy też tak macie, że lubicie nie tylko znać składnię i to, w jaki sposób coś działa, ale jeszcze rozumieć, co to w zasadzie oznacza. Dziś tłumaczymy keyof any.
Ok, rzućmy okiem na ten przykład Record:
type Record<K extends keyof any, T> = {
[P in K]: T;
}
Już to robiliśmy, podajemy w pierwszym klucze, w drugim typ wartości, dostajemy taki typ, który ma mieć klucze jak w pierwszym a zwracać mają typ T.
Generalnie w naszym przykładzie zrobiliśmy to tak:
type Streams = 'salary' | 'bonus' | 'sidehustle'
type MyRecord<K extends string|number|symbol, T> = {
[P in K]: T;
};
type Incomes = MyRecord<Streams, number>
// type Incomes = {
// salary: number;
// bonus: number;
// sidehustle: number;
// }
const monthlyIncomes: Incomes = {
salary: 500,
bonus: 100,
sidehustle: 250
}
for (const revenue in monthlyIncomes) {
console.log(monthlyIncomes[revenue as keyof Incomes])
}
Jeszcze wrzucę przykład z dokumentacji jak działa Record utility type:
type CatName = "miffy" | "boris" | "mordred";
interface CatInfo {
age: number;
breed: string;
}
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
Dokładnie tak działa. Ok, porównajmy sobie nasz przykład własnego record:
type MyRecord<K extends string|number|symbol, T> = {
[P in K]: T;
};
Chyba wiadomo, że klucz to może być tylko string, number lub symbol? A T to wartość, jaka ma być pod kluczami, tam jest dalej iteracja, mapped type. Dość proste.
Teraz definicja Record, ale tak jak jest w tym wbudowanym w TSa utility type:
type Record<K extends keyof any, T> = {
[P in K]: T;
}
I to jest dokładnie to samo. Składnia 'extends keyof any’ oznacza ni mniej ni więcej niż każdy typ, który może być kluczem. Na razie te typy to symbol, number i string, aczkolwiek gdyby coś się zmieniło, to taki kod jest bardziej future-proof i twórcy biblioteczek tym się kierują.
Więcej dziwnej składni już niedługo, na moim celowniku na razie jest próba jakiegoś konceptualnego zrozumienia i ogarnięcia tego kodziku:
type FieldsNames<T extends object> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
Bo co on robi, to rozumiem, przypisuje never do każdej funkcji, pola zostawia, następnie ten [keyof T] wyłuskuje nam nazwy tych pól. I możemy to sobie użyć dalej:
type OnlyFields<T extends object> = {
[K in FieldsNames<T>]: T[K];
};
I teraz możemy na podstawie jednego typu zdefiniować inny typ, który jest tym samym, ale pozbawionym metod. Pytanie, dlaczego ta składnia działa i jaka filozofia się za nią kryje.
Bo mamy na przykład takie coś:
type Listeners<T> = {
[P in keyof T as `on${Capitalize<string & P>}Change`]: (newValue: T[P]) => void;
};
type EmpListeners = Listeners<Employee2>
// type EmpListeners = {
// onIdChange: (newValue: number) => void;
// onNameChange: (newValue: string) => void;
// onAgeChange: (newValue: number) => void;
// onSalaryChange: (newValue: number) => void;
// }
type EmpLisenersKeys = keyof EmpListeners;
// type EmpLisenersKeys = "onNameChange" | "onAgeChange" | "onIdChange" | "onSalaryChange"
No, to działa. Ale gdybyśmy tutaj chcieli użyć tej sztuczki, to będzie błąd:
type Listeners<T> = {
[P in keyof T as `on${Capitalize<string & P>}Change`]: (newValue: T[P]) => void;
}[keyof T];
// Type 'keyof T' cannot be used to index type
// '{ [P in keyof T as `on${Capitalize<string & P>}Change`]: (newValue: T[P]) => void; }'.ts(2536)
Więcej tsa niedługo…