Kuinka voin saada Typescriptin ymmärtämään, mikä on homogeenisen objektin arvotyyppi menettämättä avaimia staattisesta määritelmästä?


Kuinka voin saada Typescriptin ymmärtämään, mikä arvo-tyyppi objektilla on menettämättä pääsyä sen staattisesta määritelmästä tulevaan avaintietoon?

Alla olevassa koodissa haluan hyödyntää

  • TS tietää arvon tyypin, tässä tapauksessa funktion, joten minun ei tarvitse määritellä sen allekirjoitusta joka kerta.
  • TS tietää objektin avaimet staattisesta määritelmästä, jotta voin viitata niihin tyypin mukaisesti turvallisella tavalla muualta

Tämä objekti on homogeeninen ja sillä on paljon toimintoja, joten allekirjoituksen määrittäminen joka kerta tuntuu vaivalta.

Esimerkki 1 - TS ei tiedä objektin avaimia

    interface Foo {
        [key: string]: (n: number) => any
    }
    
    const obj1: Foo = {
        // TS already knows that `n` is a number 🙂
        bar(n) {return n}
    }
    
    type KeyOfObj1 = keyof typeof obj1
    const kob1: KeyOfObj1 = "xyz" // This does not fail static type checking 😢
    

Esimerkki 2 - TS ei tiedä niiden funktioiden allekirjoituksia, jotka ovat objektin arvoja

    const obj2 = {
        // Notice here I have to specify the type of `n` 😢
        bar(n: number) {return n}
    }
    
    type KeyOfObj2 = keyof typeof obj2
    const kob2: KeyOfObj2 = "xyz" // This fails static type checking 🙂

Kuinka voin saada parhaan kummastakin maailmasta?

p.s. Ymmärrän, että voisin siirtää avaimet enumiksi, mutta se tuntuu melko rumalta ja toistavalta, vaikka ehkä se todella on ainoa tapa 😕

Vastaus #1

Tämä on melko yleinen tilanne: haluat kirjoittaa objektin, jonka jäsenet ovat kontekstuaalisesti tyypitettyjä jonkin heikomman tyypin mukaan (tässä tapauksessa Foo), mutta säilyttää silti vahvemman tyypin johonkin muuhun tarkoitukseen (tässä tapauksessa et halua "unohtaa" mitä avaimet ovat).

Tavallista on kirjoittaa geneerinen identiteettifunktio, kuten alla, joka tarkistaa, että argumenttityyppi T on Foon alityyppi, samalla kun se palauttaa tiukemman tyypin T:

function foo<T extends Foo>(obj: T): T {
    return obj;
}

Sitten kun luot objektin, kääntäjä määrittää sen kontekstuaalisesti tyypiksi ikään kuin se olisi jokin Foo-aliluokka, mutta säilyttää silti kyseisen aliluokan (sen erityisillä avaimen nimillä) obj-muuttujalle tehtävää määritystä varten:

const obj = foo({
    foo(n) { return n + 1; },
    bar(n) { return n * 2; },
});

// 'foo' | 'bar'
type ObjKeys = keyof typeof obj

Leikkipuiston linkki

Valitettavasti ainakin tällä hetkellä tarvitset geneerisen identiteettifunktion, jotta tämä toimisi. Tällä hetkellä on ehdotus lisätä uutta syntaksia kieleen, joka mahdollistaisi tämänkaltaisen ongelman ratkaisemisen ilman "turhan" ylimääräisen funktion lisäämistä.

Aiheeseen liittyviä artikkeleita