4

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?

qqilihq
  • 10,794
  • 7
  • 48
  • 89

1 Answers1

3

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

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357