Hvordan sjekke om et sterkt typet objekt inneholder en gitt nøkkel i TypeScript uten at det klager?


Ta denne koden:

const lookup = {
    foo: 1,
    bar: 2
}

const getValueOrDefault = (name: string, defaultValue: number) => {
    if (name in lookup) {
        return lookup[name] // ERROR
    }

    return defaultValue
}

Uttrykket lookup[name] forårsaker denne TS-feilen (playground):

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: number; bar: number; }'.
  No index signature with a parameter of type 'string' was found on type '{ foo: number; bar: number; }'.

...selv om jeg gjorde if (name in lookup) for å sjekke det først. Jeg prøvde hasOwnProperty, og det fungerer heller ikke.

TypeScript er ofte smart når det gjelder å raffinere typer innenfor betingede blokker, men ikke her. Hvorfor? Og hvordan kan jeg få det til å fungere (uten bare å jukse ved å slappe av typen til lookup)?

Svar #1

Du kan pakke name in lookup sjekken inn i en typebeskytter for å hjelpe TypeScript å forstå at når betingelsen er sann - er name en gyldig nøkkel for lookup:

const getValueOrFalse = (name: string) => {
    if (isObjKey(name, lookup)) {
        // name narrowed to "foo" | "bar"
        return lookup[name]
    }

    return false
}

function isObjKey<T>(key: PropertyKey, obj: T): key is keyof T {
    return key in obj;
}

Lekeplass

Svar #2

Du kan gjøre hva du vil med denne typevakten, kopiert rett fra det aksepterte svaret:

function isObjKey<T>(key: any, obj: T): key is keyof T {
    return key in obj;
}

Vennligst merk at som en frittstående funksjon er dette galt i TypeScript gitt det faktum at objekttyper er åpne. Du kan få merkelig typing med dette. Dette for eksempel er gyldig kode:

interface X = { a: number;}
const x = { a: 0, b: 'hello' }
const y: X = { a: 0 };
const z: X = x;
const key: string = 'b';
if (isObjKey(key, z)) {
    // here, key is typed as 'a', but key === 'b'
    const a = z[key]; // oh no, a string statically typed as number
    const b = x[key]; // again, a string statically typed as number
    const c = y[key]; // and here we have undefined typed as number
    const e = c.toString(); // crash, TypeError: cannot read property 'value' of undefined
}

Poenget er at key in obj tester for egenskaper i objektets dynamiske type, ikke i den statiske typedefinisjonen. Og at typer er åpne i TypeScript betyr at ethvert objekt kan følge et grensesnitt selv om det har flere egenskaper enn nødvendig.

Siden key in obj ikke innebærer key is keyof typeof obj og TypeScript ikke lar deg gjøre disse feilene fritt, med if (key in obj) { ... }. Må du skrive denne ødelagte typevokteren manuelt og ta alt ansvar som følger med den. Dette er samme grunn til at Object.keys(x) returnerer en string[] i stedet for en Array<keyof typeof x> (eller en mer komplisert kartlagt type for å kun inkludere string-nøkler).

Når det er sagt, i noen tilfeller kan du finne denne typen ødelagte typebeskyttelsesfunksjoner nyttige, men du må være sikker på at den dynamiske typen til objektet du sender som parameter samsvarer med dens statiske type. Og av denne grunn synes jeg at den beste tilnærmingen er å skrive en eksplisitt typepåstand, med en kommentar som forklarer hvorfor påstanden gjelder i dette spesifikke tilfellet, noe som er mindre sannsynlig å passere kodegjennomgang umerket:

const getValueOrDefault = (name: string, defaultValue: number) => {
    if (name in lookup) {
        // lookup never has more keys than what explicitly
        // written in its static type because blah blah blah...
        // so this assertion is safe
        return lookup[name as keyof lookup]
    }

    return defaultValue
}

Relaterte artikler