Search code examples
typescripttypeguards

Dynamic type guard functions


I would like to create a type guard function which checks if specific keys in an object have e.g. a string value. This is easy enough if I know the key names:

const data: object = { a: 'one', b: 'two', c: 'three' };

function hasStringKeyA(data: any): data is { a: string } {
    return typeof data.a === 'string';
}

if (hasStringKeyA(data)) {
    console.log(data.a);
}

But when I need to check for more keys, this gets messy:

if (hasStringKeyA(data) && hasStringKeyB(data)) {
    console.log([ data.a, data.b ].join(', '));
}

What I would like to do instead:

if (hasStringKeys(data, ['a', 'b', 'c'])) {
    console.log([ data.a, data.c, data.b ].join(', '));
}

But I cannot figure out how to write a type guard function which can be parametrized this way. Here’s a lousy attempt which does not work:

function hasStringKeys <K extends string[]> (data: any, keys: Keys): data is { [ k: typeof K ]: string } {
    for (const key of keys) {
        if (typeof data[key] !== 'string') {
            return false;
        }
    }
    return true;
}

Is this possible? How?


Solution

  • You are pretty close. I would use a type parameter to capture the string literal type of the items instead of the whole string array. And you need to use a mapped type instead of an index signature, you could write it yourself ({ [ k in K ]: string }) but the predefined Record type should work as well:

    const data: object = { a: 'one', b: 'two', c: 'three' };
    
    if (hasStringKeys(data, ['a', 'b', 'c'])) {
        console.log([ data.a, data.c, data.b ].join(', '));
    }
    
    function hasStringKeys <K extends string> (data: any, keys: K[]): data is Record<K, string> {
        for (const key of keys) {
            if (typeof data[key] !== 'string') {
                return false;
            }
        }
        return true;
    }
    

    Playground Link