Typescript - key/property type guard

Viewed 8801

Can I create a typeguard which asserts that a particular property exists (or has a specific type) in an object.

I.e

I have an interface Foo:

interface Foo {
    bar: string;
    baz: number;
    buzz?: string;
}

Now an object of type Foo will have an optional property buzz. How would I write a function which asserts that buzz exists: i.e

const item: Foo = getFooFromSomewhere();

if (!hasBuzz(item)) return;

const str: string = item.buzz; 

How would I implement hasBuzz()?. Something along the lines of a typeguard:

function hasBuzz(item: Foo): item.buzz is string {
    return typeof item.buzz === 'string'
}

Does something like this exist?

PS: I understand I can just do:

const item = getFooFromSomewhere();

if (typeof item.buzz === 'string') return;

const str: string = item.buzz; 

But my actual use-case requires me to have a separate function which asserts the existence of buzz.

4 Answers

I don't like the existing answers here because they are all specific to checking a Foo object, but you can define a hasBuzz typeguard that will check any object to see if it has a buzz property.

interface Buzzable {
    buzz: string;
}

function hasBuzz<T extends {buzz?: any}>(obj: T): obj is T & Buzzable {
    return typeof obj.buzz === "string";
}

By using a generic T for the input and returning obj is T & Buzzable rather than just obj is Buzzable you won't lose any information about specific interfaces such as Foo when checking with hasBuzz.

If hasBuzz(item: Foo) is true, then typescript knows that the type of item is Foo & Buzzable. In this case that is the same as Required<Foo> since Foo has an optional buzz property, but you can check any object. hasBuzz({}) is perfectly valid, and should always return false.

Typescript Playground Link

The point of a guard is to allow you to narrow down when you're not sure what the type is:

What might work in your case is:

interface Foo {
    bar: string;
    baz: number;
    buzz?: string;
}


function hasBuzz(item: Foo|Required<Foo>): item is Required<Foo> {
    return typeof item.buzz === 'string'
}

const f : Foo = {
    bar: 'a',
    baz: 1,
    buzz: 'x'
};


const str : string = f.buzz; // error

if (hasBuzz(f)) {
    const str2 : string = f.buzz; // works
}

Required is a helper type that given another type will return that type with all properties required (available since ts 2.8). This will narrow down your item variable as being of type Required<Foo>

You can use definite assignment assertions when you reference the property.

function hasBuzz(item: Foo): string {
    return item.buzz!;
}

If you want to treat the object as definitely having buzz further on in the code, you can narrow the type:

interface DefinitelyBuzzed extends Foo {
    buzz: string;
}

This should work:

function hasBuzz(item: Foo): item is Foo & Required<Pick<Foo, 'buzz'>> {
    return !!item.buzz
}
Related