In the following TypeScript code, I expect contact to be narrowed down to GroupContact due to the type guard in place, but contact is still Contact | GroupContact. Why?
type Contact = {
id: string;
name: string;
contactInfo: {
isStarred: boolean;
}
}
type GroupContact = Omit<Contact, "contactInfo">;
function isGroupContact(contact: Contact | GroupContact): contact is GroupContact {
return !("contactInfo" in contact);
}
function foo(contact: Contact | GroupContact) {
if (isGroupContact(contact)) {
console.log("group", contact); // why contact is `Contact | GroupContact` here and not `GroupContact`?
} else {
console.log("not group", contact);
}
}
Because TypeScript uses structural typing. Consider this example from the handbook:
The compiler is operating the same way on the code you showed as it does in the example: the derived type
GroupContactlooks like this when expanded:TS Playground
The
GroupContacttype doesn't restrict a propertycontactInfofrom being present — it just means that the type must have the propertiesidandnameof typestring— which means that any value of typeContactis also assignable toGroupContact, so the type guard doesn't actually narrow.To indicate that the type "must not have a defined value at the property
contactInfo", you can use an optional property of typenever:…and narrowing will work as expected:
Code in TS Playground
See also:
exactOptionalPropertyTypes