The following may be a bit obvious for some. But something just clicked in my head and I thought I'd write it down.
Imagine we write a function that returns the "oldest" item in a set/array:
function getOldest(items: Array<{ age: number }>) {
return items.sort((a, b) => b.age - a.age)[0];
}
This function can be called with an array of any kind of objects as long as they contain an age
property of type number
.
To help reasoning about it, let's give this a name:
type HasAge = { age: number };
Now our function can be annotated like this:
function getOldest(items: HasAge[]): HasAge {
return items.sort((a, b) => b.age - a.age)[0];
}
Great! Now we can use this function with any objects that conform to the HasAge
interface:
const things = [{ age: 10 }, { age: 20 }, { age: 15 }];
const oldestThing = getOldest(things);
console.log(oldestThing.age); // 20 ✅
Because the type of oldestThing
is inferred to be HasAge
, we can access its .age
property.
But what if we have more complex types?
type Person = { name: string, age: number};
const people: Person[] = [
{ name: 'Amir', age: 10 },
{ name: 'Betty', age: 20 },
{ name: 'Cecile', age: 15 }
];
const oldestPerson = getOldest(people); // 🙂 no type errors
This works, but now the inferred type of oldestPerson
is HasAge
. We've lost the Person
type along the way. As a result we can't (safely) access it's .name
property.
console.log(oldestPerson.name); // ❌ type error: Property 'name' does not exist on type 'HasAge'.
Annotating oldestPerson:Person
won't work:
const oldestPerson: Person = getOldest(people); // ❌ type error
// Property 'name' is missing in type 'HasAge' but required in type 'Person'.
We could use type assertions, but it's not a good idea. (why?)
const oldestPerson = getOldest(people) as Person; // 🚩
console.log(oldestPerson.name); // no type error
You can try this in TypeScript Playground.
We can do better. We can turn this into a generic function.
function getOldest<T extends HasAge>(items: T[]): T {
return items.sort((a, b) => b.age - a.age)[0];
}
const oldestPerson = getOldest(people); // ✅ type Person
Success!! Now the inferred type of oldestPerson
is Person
!
As a result we can access its .name
property.
Here's another example with 2 different types:
type Person = {name: string, age: number};
const people: Person[] = [
{ name: 'Amir', age: 10 },
{ name: 'Betty', age: 20 },
{ name: 'Cecile', age: 15 }
];
type Bridge = {name: string, length: number, age: number};
const bridges = [
{ name: 'London Bridge', length: 269, age: 48 },
{ name: 'Tower Bridge', length: 244, age: 125 },
{ name: 'Westminster Bridge', length: 250, age: 269 }
]
const oldestPerson = getOldest(people); // type Person
const oldestBridge = getOldest(bridges); // type Bridge
console.log(oldestPerson.name); // 'Betty' ✅
console.log(oldestBridge.length); // '250' ✅
You can try this in TypeScript Playground
Even if your function takes objects conforming to the HasAge
interface; as long as you don't mean to return the same type you don't need generics.
function isFirstOlder<T extends HasAge>(a: T, b: T) {
return a.age > b.age;
}
The function above doesn't need to be generic. We can simply write:
function isFirstOlder(a: HasAge, b: HasAge) {
return a.age > b.age;
}
- Generics (TypeScript Handbook)
- Type Assertions (TypeScript Handbook)
- Type Assertion (Basarat's TypeScript Deep Dive)
- TypeScript Playground
I'd love to hear your thoughts. Do you have other use cases? examples?