8

Take this code:

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

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

    return defaultValue
}

The lookup[name] expression causes this TS error (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; }'.

...even though I did if (name in lookup) to check it first. I tried hasOwnProperty, and that doesn't work either.

TypeScript is often smart about refining types within conditional blocks, but not here. Why? And how can I make it work (without just hacking it by relaxing the type of lookup)?

callum
  • 34,206
  • 35
  • 106
  • 163
  • 1
    You need to use `keyof` to ensure`name` is a key of `lookup`: http://www.typescriptlang.org/play/?ssl=9&ssc=6&pln=10&pc=1#code/MYewdgzgLgBANiEBrArgBxgXhgbwFAyEwBmiAXDAIwA0BRARgIYBOFATHgL556iSwBzAKZQAaozgohAeWYAxCRCFYYACjCMAtkIpIhATxDEYUfWiFH4iVGgCUWAHy46hAJbH1W5a7BXk6e3wiYJhmERRmXwR-NABtDW0AXRcYbmCwqAjfYkUhLjwgA – Titian Cernicova-Dragomir Nov 20 '19 at 17:26
  • 2
    @Titian - but that would be wrong, because the name variable could be any string, hence the need for my runtime check. – callum Nov 20 '19 at 17:59
  • Oh yes sorry missed that. Since you perform the check you can type assert that `name` is `keyof typeof lookup`. This is mostly safe (See why it might not be here https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript/55012175#55012175) http://www.typescriptlang.org/play/#code/MYewdgzgLgBANiEBrArgBxgXhgbwFAyEwBmiAXDAIwA0BRARgIYBOFATHgL556iSwBzAKZQAaozgohAeWYAxCRCFYYACjCMAtkIrRmASzACAlFgB8uOoX3E1G7TEPxEqNKfxFPMZiJTMwzsjoANr2yowQMEhCAJ4gtlAxaELxga4AulYw3J4+UH4BxIpCXHhAA – Titian Cernicova-Dragomir Nov 20 '19 at 18:30

3 Answers3

13

You can wrap the name in lookup check in type guard to help typescript to understand that once condition is true - name is valid lookup's key:

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;
}

Playground

Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
5

You can do what you want with this typeguard, copied straight from the accepted answer:

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

However please note that as standalone function this is wrong in typescript given the fact that object types are open. You can get weird typing with this. This for example is valid code:

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
}

The point is that key in obj tests for properties in the dynamic type of the object, not in the static type definition. And types being open in typescript mean that any object adhere to an interface even if it has more properties than needed.

Since key in obj doesn't imply key is keyof typeof obj and typescript doesn't let you do these mistakes freely, with if (key in obj) { ... }. You have to write this broken typeguard manually and assume all responsibility that comes with it. This is the same reason for which Object.keys(x) returns a string[] instead of an Array<keyof typeof x> (or a more complicated mapped type to only include string keys).

That said, in some cases you may find this kind of broken type guard functions useful, but you need to be certain that the dynamic type of the object you are passing as parameter coincides with its static type. And for this reason I find that the best approach is to write an explicit type assertion, with a comment explaining why the assertion holds in this particular case, which is less likely to pass code review unnoticed:

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
}
pqnet
  • 6,070
  • 1
  • 30
  • 51
  • Excellent explanation! Is there any way to do the `key is keyof typeof obj` check? I.e. if I know that `obj: T` may have more keys but want to check if the `key: string` is one of the known keys? – Samuli Asmala Dec 08 '21 at 08:58
  • 1
    @SamuliAsmala you can dynamic typecheck only a limited set of types, so there is no generic way to do that, but you can check if obj has a key of type string/number using: `typeof obj === 'object' && obj !== null && typeof (obj as any).key === 'string'`. If you find yourself doing this often you may need to extract it to a function – pqnet Dec 09 '21 at 15:48
-1

According to this answer: TypeScript TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'

I think you could do something like: const getValueOrFalse = ({name: string}: any) => {

(It's different than what you posted in here because I used your playground to test it, but I think you can work it out.)

Hope that works for you.

João Silva
  • 531
  • 4
  • 21
  • 40