You have two questions:
How do you check requiresWalks to see if the animal requires a walk?
How do you define the type for animal in goForAWalkWith?
Re #1: You test to see if the object has the property before you try to use it (this is a specific type of narrowing the handbook calls in operator narrowing):
animals.forEach(animal => {
if ("requiresWalks" in animal && animal.requiresWalks) {
// −−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
goForAWalkWith(animal)
}
});
Re #2, you can extract from the Animal union all types that are assignable to {requiresWalks: true} via the Extract utility type:
function goForAWalkWith(animal: Extract<Animal, {requiresWalks: true}>) {
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ...
}
Extract<Animal, {requiresWalks: true}> is Dog | Wolf.
Playground link
You con't have to do that inline if you don't want to, you can define a type alias for it, then use the alias:
type AnimalThatRequiresWalks = Extract<Animal, {requiresWalks: true}>;
// ...
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
// ...
}
Playground link
In the comments, you've said you'd like to define all of the AnimalThatRequiresWalks properties together in an explicit type for it rather than inferring that type from Dog, Cat, etc. You can do that:
interface AnimalThatRequiresWalks {
animal: string;
requiresWalks: true;
preferredWalkTerrain: "hills" | "paths" | "woods";
walkDistancePerDayKm: number;
}
export type Cat = {
animal: "CAT";
legs: 4;
};
export type Dog = AnimalThatRequiresWalks & {
animal: "DOG";
legs: 4;
walkDistancePerDayKm: 5; // Only needed if you want to refine its type
preferredWalkTerrain: "paths"; // Same
};
export type Wolf = AnimalThatRequiresWalks & {
animal: "WOLF";
legs: 4;
walkDistancePerDayKm: 20; // Only needed if you want to refine its type
preferredWalkTerrain: "woods"; // Same
}
type Animal = Cat | Dog | Wolf;
declare const animals: Animal[]; // = getAnimals();
animals.forEach(animal => {
if ("requiresWalks" in animal && animal.requiresWalks) {
goForAWalkWith(animal)
}
});
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
console.log("Go for a walk with the " + animal.animal);
}
Playground link
You might want to experiment with that to see what the developer experience is like. It's really easy to forget to put that AnimalThatRequiresWalks & part on when defining Dog, Wolf, etc., and the type of AnimalThatRequiresWalks["animal"] is string whereas when you infer it is it's the more narrow "Cat" | "Dog" | "Wolf". But it may be more convenient.
It occurs that one way to address the ease of forgetting AnimalThatRequiresWalks & at the beginning is to use a generic type for animals:
interface AnimalThatRequiresWalks {
animal: string;
requiresWalks: true;
preferredWalkTerrain: "hills" | "paths" | "woods";
walkDistancePerDayKm: number;
}
type AnimalType<R extends boolean, Type extends object> =
R extends true
? AnimalThatRequiresWalks & Type
: Type;
export type Cat = AnimalType<false, {
animal: "CAT";
legs: 4;
}>;
export type Dog = AnimalType<true, {
animal: "DOG";
legs: 4;
walkDistancePerDayKm: 5; // Only needed if you want to refine its type
preferredWalkTerrain: "paths"; // Same
}>;
export type Wolf = AnimalType<true, {
animal: "WOLF";
legs: 4;
walkDistancePerDayKm: 20; // Only needed if you want to refine its type
preferredWalkTerrain: "woods"; // Same
}>;
type Animal = Cat | Dog | Wolf;
declare const animals: Animal[]; // = getAnimals();
animals.forEach(animal => {
if ("requiresWalks" in animal && animal.requiresWalks) {
goForAWalkWith(animal)
}
});
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
console.log("Go for a walk with the " + animal.animal);
}
Playground link
Maybe that's better, or maybe it's over-engineered. :-D