DEV Community

Cover image for Typescript: Are you over-typing your functions?
Red Ochsenbein
Red Ochsenbein

Posted on • Originally published at ochsenbein.red

Typescript: Are you over-typing your functions?

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.


Image: Photo by ๅ‚…็”ฌ ๅŽ on Unsplash

Discussion (5)

Collapse
joelbonetr profile image
JoelBonetR

// @ts-ignore also does a great job ๐Ÿ˜‚
JK, the defined in your post is a valid option

Collapse
syeo66 profile image
Red Ochsenbein Author

Don't get me started on @ts-ignore or 'any'... ๐Ÿ˜‚

Collapse
joelbonetr profile image
JoelBonetR

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 ๐Ÿ˜…

Thread Thread
syeo66 profile image
Red Ochsenbein Author

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 to as ... 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.

Thread Thread
joelbonetr profile image
JoelBonetR

Indeed