When defining a function in Typescript, the first instinct might tell you to use your existing interfaces or even classes. This might not be optimal. Let's start with a simple example:
interface Cat {
name: string;
furColor: string;
age: number;
}
const getName = (cat: Cat): string => {
return cat.name;
}
const cat: Cat = { name: 'Garfield', furColor: 'orange', age: 3 }
console.log(getName(cat));
In this example, the function getName()
returns the name of the cat (Garfield). So far so good. Now let's assume we introduce a new interface Person
:
interface Person {
name: string;
age: number;
}
const person: Person = { name: 'John', age: 30 }
console.log(getName(person));
Now typescript would complain about the type of person
:
Argument of type 'Person' is not assignable to parameter of type 'Cat'.
Property 'furColor' is missing in type 'Person' but required in type 'Cat'.
This makes sense because a person is obviously not a cat (in most cases). But this is too restricting for a function which in actuallity only uses the name of the object it receives.
Make the function more flexible
The solution is to rewrite the function to make it more flexible:
const getName = (obj: { name: string }): string => {
return obj.name;
}
Now we can use it for cats and people, and every other object which has a name
property. The function does not need anything else to work with, so we should no restrict it artificially.
But why does this work? It works because Typescript only checks if an argument does at least have the properties defined in the interface. It does not compare the actual type and ignores any additional properties in the argument. This also means I had to construct the example this way around to demonstrate my point. If I had used Person
instead of Cat
as the type of the parameter, the compiler would have accepted a cat as well. But still, the function is only actually using the name
property, so why should we expect the parameter to contain an age?
This might seem obvious in this case. But I have seen a lot of more complex functions where the types of the parameters were quite restricting while not even using all the properties in the function.
One more step
We can do even better by using generics.
const getName = <T extends { name: unknown }>(obj: T): T['name'] => {
return obj.name;
}
This way we don't even have to know the type of name
. It will be inferred from the type of the object. Of course in this special case, it does not make much sense to have a name be anything else than a string. But it helps to highlight the power of generics.
Conclusion
This is all I wanted to show you in this article. Don't just slap your existing interfaces on the parameters of a function. Think about what the function actually needs and only requires this. It will make your functions more flexible and easier to reuse.
Generics are a powerful tool to help make your code even more reusable. They are certainly worth exploring even further. But this will be a topic for another time.
Top comments (5)
// @ts-ignore
also does a great job 😂JK, the defined in your post is a valid option
Don't get me started on @ts-ignore or 'any'... 😂
I find it funny because the "maybe", "unknown" and all those types are getting more visibility this days in the community plus I saw many posts about generics and so...
For me it's like... Dude, you're tired of defining all that BS. I get it. I understand it. Just use JS, you'll be fine 😅
Well, I do have to say that for one the Maybe monad is a thing I really like in functional programming. It's way better than having unhandled
null
values all over the place.Also
unknown
is probably the best solution if you really don't know (or care) about a type. It forces the receiver of the type to actually test for the validity of a value (except of course the dev just resorts toas ...
or worse@ts-ignore
). And this is IMO the value of typescript: It forces you to think about what values you actually can expect and what possible error states you have to deal with.But then, if you don't like using Typescript in strict mode, or tend to resort to the the easy way out (like
any
,as ...
or@ts-ignore
) you better don't use Typescript because it would only make things worse.Indeed