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…