Prečo Object.keys nevracia keyof typ v TypeScripte?


Názov hovorí za všetko - prečo Object.keys(x) v TypeScripte nevracia typ Array<keyof typeof x>? To je presne to, čo Object.keys robí, takže sa zdá byť očividným prehliadnutím zo strany autorov súborov s definíciami TypeScript, že návratový typ nie je jednoducho keyof T.

Mám nahlásiť chybu na ich GitHub repozitári, alebo sa mám rovno pustiť do poslania PR na opravenie toho za nich?

Odpoveď č. 1

Súčasný návratový typ (string[]) je zámerný. Prečo?

Zvážte niektorý typ ako tento:

interface Point {
    x: number;
    y: number;
}

Napíšete nejaký kód takto:

function fn(k: keyof Point) {
    if (k === "x") {
        console.log("X axis");
    } else if (k === "y") {
        console.log("Y axis");
    } else {
        throw new Error("This is impossible");
    }
}

Poďme sa opýtať otázku:

V dobre typovanom programe, môže legálne volanie fn skončiť chybou?

Žiadaná odpoveď je, samozrejme, „Nie“. Ale čo to má spoločné s Object.keys?

Teraz zvážte tento iný kód:

interface NamedPoint extends Point {
    name: string;
}

const origin: NamedPoint = { name: "origin", x: 0, y: 0 };

Všimnite si, že podľa typového systému TypeScriptu sú všetky NamedPoint platnými Point.

Teraz napíšme ešte trochu kódu:

function doSomething(pt: Point) {
    for (const k of Object.keys(pt)) {
        // A valid call if Object.keys(pt) returns (keyof Point)[]
        fn(k);
    }
}
// Throws an exception
doSomething(origin);

Náš dobre typovaný program práve vyhodil výnimku!

Niečo tu nešlo správne! Vrátením keyof T z Object.keys sme porušili predpoklad, že keyof T tvorí vyčerpávajúci zoznam, pretože samotná referencia na objekt neznamená, že typ referencie nie je nadtypom typu hodnoty.

V podstate nemôže byť pravdivé (aspoň) jedno z nasledujúcich štyroch tvrdení:

  1. keyof T je vyčerpávajúci zoznam kľúčov T
  2. Typ s dodatočnými vlastnosťami je vždy podtypom svojho základného typu
  3. Je legálne aliasovať hodnotu podtypu prostredníctvom referencie nadtypu
  4. Object.keys vracia keyof T

Zahodenie bodu 1 robí keyof takmer neužitočným, pretože to naznačuje, že keyof Point môže byť nejaká hodnota, ktorá nie je "x" ani "y".

Úplné vynechanie bodu 2 úplne zničí typový systém TypeScriptu. Nie je to možnosť.

Zahodenie bodu 3 úplne ničí aj typový systém TypeScriptu.

Zahodenie bodu 4 je v poriadku a prinúti vás, programátora, zamyslieť sa nad tým, či objekt, s ktorým pracujete, nie je možné aliasom pre podtyp toho, čo si myslíte, že máte.

„Chýbajúca funkcia“, ktorá by to urobila legálnym, ale nie protirečivým, sú Presné Typy, ktoré by vám umožnili deklarovať nový druh typu, ktorý by nepodliehal bodu č. 2. Keby táto funkcia existovala, pravdepodobne by bolo možné, aby Object.keys vracala keyof T iba pre T, ktoré boli deklarované ako presné.


Dodatok: Určite generiká, však?

Komentátori naznačili, že Object.keys by mohol bezpečne vrátiť keyof T, ak by argument bol generická hodnota. To je však stále nesprávne. Zvážte:

class Holder<T> {
    value: T;
    constructor(arg: T) {
        this.value = arg;
    }

    getKeys(): (keyof T)[] {
        // Proposed: This should be OK
        return Object.keys(this.value);
    }
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// Value 'name' inhabits variable of type 'x' | 'y'
const v: "x" | "y" = (h.getKeys())[0];

alebo tento príklad, ktorý dokonca nepotrebuje žiadne explicitné typové argumenty:

function getKey<T>(x: T, y: T): keyof T {
    // Proposed: This should be OK
    return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// Value "name" inhabits variable with type "x" | "y"
const s: "x" | "y" = getKey(obj1, obj2);

Odpoveď č. 2

Ako obchádzkové riešenie v prípadoch, keď ste si istí, že v objekte, s ktorým pracujete, nie sú žiadne dodatočné vlastnosti, môžete urobiť toto:

const obj = {a: 1, b: 2}
const objKeys = Object.keys(obj) as Array<keyof typeof obj>
// objKeys has type ("a" | "b")[]

Môžete to vyextrahovať do funkcie, ak chcete:

const getKeys = <T>(obj: T) => Object.keys(obj) as Array<keyof T>

const obj = {a: 1, b: 2}
const objKeys = getKeys(obj)
// objKeys has type ("a" | "b")[]

Ako bonus, tu je Object.entries, prevzaté z problému na GitHub so súvislosťou o tom, prečo toto nie je predvolené:

type Entries<T> = {
  [K in keyof T]: [K, T[K]]
}[keyof T][]

function entries<T>(obj: T): Entries<T> {
  return Object.entries(obj) as any;
}

Odpoveď č. 3

Toto je najpopulárnejší výsledok na Google pre tento typ problému, takže som chcel zdieľať nejakú pomoc pri postupe ďalej.

Tieto metódy boli väčšinou prevzaté z dlhých diskusií na rôznych stránkach problémov, na ktoré môžete nájsť odkazy v iných odpovediach/komentárových sekciách.

Takže povedzme, že by ste mali nejaký kód ako tento:

const obj = {};
Object.keys(obj).forEach((key) => {
  obj[key]; // blatantly safe code that errors
});

Tu je niekoľko spôsobov, ako postupovať ďalej:

  1. Ak kľúče nepotrebujete a naozaj potrebujete iba hodnoty, použite .entries() alebo .values() namiesto iterovania cez kľúče.

    const obj = {};
    Object.values(obj).forEach(value => value);
    Object.entries(obj).forEach([key, value] => value);
    
  2. Vytvorte pomocnú funkciu:

    function keysOf<T extends Object>(obj: T): Array<keyof T> {
      return Array.from(Object.keys(obj)) as any;
    }
    
    const obj = { a: 1; b: 2 };
    keysOf(obj).forEach((key) => obj[key]); // type of key is "a" | "b"
    
  3. Prepracujte svoj typ (toto veľmi pomáha, aby ste nemuseli prepisovať veľa kódu)

    const obj = {};
    Object.keys(obj).forEach((_key) => {
      const key = _key as keyof typeof obj;
      obj[key];
    });
    

Ktorý z týchto je najmenej bolestivý, závisí do značnej miery od vášho vlastného projektu.

Súvisiace články