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:
let o = { x: "hi", extra: 1 }; // ok let o2: { x: string } = o; // ok
Here, the object literal
{ x: "hi", extra: 1 }
has a matching literal type{ x: string, extra: number }
. That type is assignable to{ x: string }
since it has all the required properties and those properties have assignable types. The extra property doesn’t prevent assignment, it just makes it a subtype of{ x: string }
.
The compiler is operating the same way on the code you showed as it does in the example: the derived type GroupContact
looks like this when expanded:
type GroupContact = Omit<Contact, "contactInfo">;
/* ^?
type GroupContact = {
id: string;
name: string;
}
*/
The GroupContact
type doesn't restrict a property contactInfo
from being present — it just means that the type must have the properties id
and name
of type string
— which means that any value of type Contact
is also assignable to GroupContact
, 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 type never
:
type GroupContact = Omit<Contact, "contactInfo"> & { contactInfo?: never };
…and narrowing will work as expected:
function foo(contact: Contact | GroupContact) {
if (isGroupContact(contact)) {
console.log("group", contact);
// ^? (parameter) contact: GroupContact
} else {
console.log("not group", contact);
// ^? (parameter) contact: Contact
}
}
See also: exactOptionalPropertyTypes