Is it possible to narrow object value types when accessing object with varied value types using a dynamic key?

Minimal test case follows (TS playground link)

interface TestObject {
    a: string
    b: number
}

type TestKeyType = keyof TestObject

const myObjct = {a: "cat", b:34} as TestObject;
const key = 'a' as TestKeyType;

if (typeof myObjct[key] !== 'string') {
    throw TypeError();
}

// Error: Type 'string' is not assignable to type 'never'.
myObjct[key] = 'dog';

Essentially, I have a configuration object where the values can be various types. When I update this object, the key depends on user input but based on the category of input I can know if it's a boolean or number or string etc.

As shown, I can write a type guard which should guarantee what type the dynamic key corresonds to but Typescript disagrees.

Is there something else concise I can do to prove to Typescript that my type is valid?

I know I can enumerate the keys that correspond to each value type to check but it seems a bit verbose and redundant; I'd prefer to minimize the places I need to update when expanding the keys in the object.

1 answer

  • answered 2021-05-17 06:06 kaya3

    You can write a user-defined type guard for this: typeCheck(obj, key, type) will tell you whether obj[key] has the type type. The wrinkle is that type has to be passed as a string that typeof can return at runtime, so you need a mapping from those strings to Typescript types; this mapping is named Primitive below. Add whatever else you need to this mapping (e.g. bigint) as necessary.

    type Primitive = {
        number: number,
        string: string,
        boolean: boolean,
    }
    type KeyAssignableTo<T, V> = {[K in keyof T]: T[K] extends V ? K : never}[keyof T]
    
    function typeCheck<T, P extends keyof Primitive>(obj: T, key: keyof T, type: P): key is KeyAssignableTo<T, Primitive[P]> {
        return typeof obj[key] === type;
    }
    

    Usage:

    if(!typeCheck(myObject, key, 'string')) {
        throw TypeError();
    }
    
    // key: 'a'
    myObject[key] = 'dog';
    

    Playground Link