TypeScript instanceof and the in Operator
instanceof narrows a value by the class the value was built from. The in operator narrows object shapes by checking that a member is present.
instanceof in a class hierarchy
Dog extends Animal, so one array holds both classes under the base type. instanceof finds the Dog instance and unlocks the breed member in that branch.
class Animal {
animalName: string;
constructor(animalName: string) {
this.animalName = animalName;
}
}
class Dog extends Animal {
breed: string;
constructor(animalName: string, breed: string) {
super(animalName);
this.breed = breed;
}
}
const pets: Animal[] = [new Animal("Creature"), new Dog("Rex", "Labrador")];
for (const pet of pets) {
if (pet instanceof Dog) {
console.log(pet.animalName + " is a dog: " + pet.breed);
} else {
console.log(pet.animalName + " is a plain animal");
}
}instanceof with a built-in class
Date is a built-in class, and instanceof works with built-in classes in the same way. The branch reads a date method on Date values and a string method on the rest.
const entries: (Date | string)[] = [new Date(Date.UTC(2024, 0, 15)), "hello"];
for (const entry of entries) {
if (entry instanceof Date) {
console.log("date " + entry.getUTCFullYear());
} else {
console.log("text " + entry.toUpperCase());
}
}The in operator narrows by presence
The in operator asks whether a member name exists on a value, and the name is written as a quoted string before in. Only the fish shape has swim, so the true branch narrows the value to the fish shape.
type Fish = { petName: string; swim: () => string };
type Bird = { petName: string; fly: () => string };
type Pet = Fish | Bird;
function move(pet: Pet): string {
if ("swim" in pet) {
return pet.petName + " " + pet.swim();
}
return pet.petName + " " + pet.fly();
}
const fish: Fish = { petName: "Nemo", swim: () => "swims fast" };
const bird: Bird = { petName: "Pip", fly: () => "flies high" };
console.log(move(fish));
console.log(move(bird));Example
class Service {
serviceName: string;
constructor(serviceName: string) {
this.serviceName = serviceName;
}
}
class PremiumService extends Service {
level: number;
constructor(serviceName: string, level: number) {
super(serviceName);
this.level = level;
}
}
type WithEmail = { contactName: string; email: string };
type WithPhone = { contactName: string; phone: string };
type Contact = WithEmail | WithPhone;
function contactLine(contact: Contact): string {
if ("email" in contact) {
return contact.contactName + " by email " + contact.email;
}
return contact.contactName + " by phone " + contact.phone;
}
const premium = new PremiumService("Hosting", 2);
console.log(premium instanceof PremiumService);
console.log(premium.serviceName);
console.log(contactLine({ contactName: "Mira", email: "mira@example.com" }));
console.log(contactLine({ contactName: "Omar", phone: "555-0100" }));Key points
- instanceof tests whether an object was built from a given class or a subclass of it.
- A subclass instance narrows to the subclass inside an instanceof branch.
- Date values in a union with strings separate cleanly with instanceof Date.
- Array values are tested with Array.isArray, the dedicated test for arrays.
- The in operator checks member presence and narrows a union to the shape that has that member.
- A member that exists on only one shape is a safe key for an in check.
In this example, instanceof confirms the premium service class, and an in check on the email member narrows each contact to the shape that carries that member.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท TypeScript Playground
Exercise
A value has a union of a fish shape with swim and a bird shape with fly. What does "swim" in pet do inside the true branch?