Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 25, 2022 02:58 pm GMT

The TypeScript Functions Mental Model

Decision diagram on typescript's functions

TypeScript Functions

When it comes to writing TypeScript functions, it is possible (and very likely), that you have experienced this sort of thinking:

This looks kinda weird, should I try another way? Maybe function overloading, or perhaps generics would help here.

And you ended up changing for one of them, without really knowing if it was the right thing.

I've been there and have certainly done this countless times - but what if there was a system that could help you solve this little nuance?

That's exactly what I'm presenting here, a system that will allow you to make a good and consistent decision on how you write a TypeScript function.

Union Types

type Pet = {  name: string;};type PetOwner = {  name: string;  pet: Pet;};function getPetName(petOrOwner: Pet | PetOwner) {  if ("pet" in petOrOwner) {    return petOrOwner.pet.name;  }  return petOrOwner.name;}

TypeScript's union types are super useful when it comes to type anything that can have multiple type definitions.

For functions in particular, it is particular useful to type it's parameters or return types. And you are perhaps using it more than you knew, because every time you define a argument as optional , it actually becomes type | undefined, meaning it is a union type underneath!

When to use

When it comes to functions, this is the pretty much the most basic strategy for typing it's parameters - so my advice is to stick to it whenever it's possible, but for being a little "basic" it has more restrains that the others strategies and it doesn't work for more complex scenarios.

Use union types when:

1- You are aware of all the possible members of each union type in the moment of the function's declaration;
2- The return type doesn't change depending on the argument's types;

type Pet = {  name: string;};type PetOwner = {  ownerName: string;  pet: Pet;};// Don't do this - you will return an ambiguous type function getObject(petOrOwner: Pet | PetOwner): Pet | PetOwner {  return petOrOwner;}// You have no idea which will be the array type function getFirstElementArray(arr: (string | number)[]): any  {  return arr[0];}

Also, when using union types for typing functions, you should most likely not need to type the return type explicitly. That's just because the return type shouldn't change at all for this way of doing things.

Function Overloading

type SingleNamePerson = {  name: string;};type FullNamePerson = {  name: string;  surname: string;};// Overload signaturesfunction getPerson(name: string): SingleNamePerson;function getPerson(name: string, surname: string): FullNamePerson;// Implementation signaturefunction getPerson(  name: string,  surname?: string): SingleNamePerson | FullNamePerson {  if (name && surname) {    return {      name,      surname,    };  }  return {    name,  };}

TypeScript's function overloads is a great way to specify multiple function signatures in a super declarative way, and making each of those have their own type definition, either for the arguments or the return type.

To leverage this way of typing a function all you need to do the following:

1- Define your functions possible signatures (the different overloads)

// Overload signaturesfunction getPerson(name: string): SingleNamePerson;function getPerson(name: string, surname: string): FullNamePerson;

2- Define your function's implementation - this function's signature must be a union between the other previously created functions, so that it respect every single overload.

// name - required because it is defined in both overloads// surname - optional, because it is only present in one of the overloads// return type - SingleNamePerson | FullNamePerson because it can be either one of thosefunction getPerson(  name: string,  surname?: string): SingleNamePerson | FullNamePerson {  if (name && surname) {    return {      name,      surname,    };  }  return {    name,  };}

When to use

Function overloading works pretty well with more difficult and dynamic signatures, as it will provides a way to define multiple combinations in a way that is easy to understand.

function overloads usage example

Use function overloading when:

1- You are aware of all the possible members of each union type in the moment of the function's declaration;
2- The return type changes depending on the argument's types;
3- The return type isn't a direct mapping of the provided parameters;

// Don't do this  - the return type is a direct mapping of the provided argument (use a generic instead)function getPerson(person: SingleNamePerson): SingleNamePerson;function getPerson(person: FullNamePerson): FullNamePerson;function getPerson(person: {  name: string;  surname?: string;}): SingleNamePerson | FullNamePerson {  const { name, surname } = person;  if (name && surname) {    return {      name,      surname,    };  }  return {    name,  };}

Generic Functions

type SingleNamePerson = {  name: string;};type FullNamePerson = {  name: string;  surname: string;};const singleNamePerson: SingleNamePerson = {  name: "Bob",};const fullNamePerson: FullNamePerson = {  name: "Bob",  surname: "Smith",};function getPerson<PersonT>(arg: PersonT): PersonT {  return arg;}getPerson(singleNamePerson); // Return type => `SingleNamePerson`getPerson(fullNamePerson); // Return type => `FullNamePerson`

TypeScript's Generic Functions is probably the most versatile way to create a function that is dynamic in some way or another.

It gives you great opportunities to get full type support on every function abstraction that has multiple case scenarios and you are not fully aware of what can be passed in as an argument ahead of time.

But all these opportunities and versatility come with a cost - as generics seem to fit everywhere, developers have a tendency to overuse it, and believe me, "no one" wants to touch a function with a huge generics complexity around, specially if it was written by another developer.

When to use

Generic Functions are the only solution you have to solve the common problem between union types and function overloading - the capacity of adding type support when we are not aware of all the possible types before-hand.

Not only that, but Generics are also a great option when the return type of a function is related with the return type (even if you are aware of them ahead of time).

// Defining the return type based on the argument's typefunction getFirstElement<T>(array: T[]): T {    return array[0];}const a = getFirstElement([1, 2, 3]); // return type => numberconst b = getFirstElement(["Hello", "World"]); // return type => stringconst c = getFirstElement([true, false]); // return type => boolean

Use generic functions when:
1- You are NOT aware of the arguments types before-hand;
2- The return type is a direct mapping (or close to it) of the provided parameters;

type Dog = {  name: string;  toy: string;};type Cat = {  name: string;  furrballs: number;};type Turtle = {  name: string;  isMainlyAquatic: boolean;};// This can get tricky pretty quickly type GetAnimalReturnType<AnimalT> = AnimalT extends Dog  ? "canine"  : AnimalT extends Cat  ? "feline"  : "turtle";// We need to use type assertions - not ideal function getAnimalType<AnimalT>(animal: AnimalT): GetAnimalReturnType<AnimalT> {  if ("toy" in animal) {    return "canine" as GetAnimalReturnType<AnimalT>;  }  if ("furrballs" in animal) {    return "feline" as GetAnimalReturnType<AnimalT>;  }  return "turtle" as GetAnimalReturnType<AnimalT>;}

Generics vs function overloading

generics vs functions overloading example

When it comes to choosing between function overloading and generics the line is blurrier and the decision isn't quite clear. As you can observe in the image above, these two TypeScript's features usage intersects when it comes to type a function that has known argument types and whose return type depend on those very same arguments.

Which to choose

The answer will depend very much on the developer's taste, but my opinion is that to keep things simple you should opt for generics instead of function overloading only when this rule applies:

The code that you need to write to define the return type doesn't have more than 1 level deepness of extends expressions.

That's the rule that I use for my self to keep code consistent and easier to others to read.

Conclusion

There are multiple techniques to write dynamic functions in TypeScript and by applying this "mental model" of doing so, you will be able to define those functions in a more consistent and clean way, while also making usage of each technique for what it was really intended.

  • Unions => Use when return type doesn't change;
  • Function Overloading => Use when you are aware of the argument types and the return type DOES CHANGE depending on the argument types;
  • Generics => Use when you are not aware of the argument types or the return type is a direct mapping of the arguments.

Make sure to follow me on twitter if you want read about TypeScript best practices or just web development in general!


Original Link: https://dev.to/pffigueiredo/the-typescript-functions-mental-model-1301

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To