I am running into an issue when I try to narrow a type based on a property. It's best if I show with some code what it it basically boils down to:
type User = {
id: number;
name: string;
}
type CreateUser = {
name?: string;
}
const user: User | CreateUser = { name: "Foo Bar" };
if (user.id) {
console.log(`Hello ${user.name}`)
}
So my thinking is that user
is either a User
or a CreateUser
, and we can see the difference based on the existence of the id
property. But sadly this doesn't work: Property 'id' does not exist on type 'CreateUser'
. And user.name
is still string | undefined
instead of a simple string
. Surely there is way to do this, but I am not able to find out how?
Even this doesn't work, which surprised me:
if ("id" in user) {
console.log(`Hello ${user.name}`)
}
I get Property 'name' does not exist on type 'never'.
.
CodePudding user response:
When using a type annotation and initializing a value, the compiler is smart enough to narrow the value to the type that you use to initialize it. When initializing it with a type that has the name
property and lacks the id
property, the compiler narrows it to the CreateUser
type:
const user: User | CreateUser = { name: 'Foo Bar' };
user;
//^? const user: CreateUser
To avoid this automatic narrowing behavior and preserve the union information in the same scope, you can use an assertion instead of an annotation:
const user = { name: 'Foo Bar' } as User | CreateUser;
if ('id' in user) {
//^? const user: User | CreateUser
console.log(`Hello ${user.name}`);
//^? const user: User
}
Response to your comment:
It works the same way when exporting a value. The important part is that every time you assign a value to the variable and want to preserve the full union type afterward in that scope, you must use an assertion to override the compiler's automatic narrowing behavior:
export let user: User | CreateUser;
user = { name: 'Foo Bar' } as User | CreateUser;
if ('id' in user) {
//^? const user: User | CreateUser
console.log(`Hello ${user.name}`);
//^? const user: User
}
and if this becomes tedious, just create an alias for the union:
export type AnyUser = User | CreateUser;
export let user: AnyUser;
user = { name: 'Foo Bar' } as AnyUser;
CodePudding user response:
This works:
function isRealUser(user: User | CreateUser): user is User {
return (user as User).id !== undefined;
}
if (isRealUser(user) {
console.log(`Hello ${user.name}`)
}
Found on https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates.