DEV Community

Nicola Zanon
Nicola Zanon

Posted on • Originally published at inextenso.dev

Progressively Typing a Function in TypeScript

When I started with TypeScript I’ve found very easy and intuitive the whole type system for the majority of the things that I wanted to accomplished.

It was a good feeling. Finally I was able to have some confidence that my code would not break at runtime and the clever Intellisense in the IDE was helping my productivity.

What does it feel like then?

Take a look at this little snippet of code.

class Hello {
    myMessage: string;
    constructor (message: string) {
        this.myMessage = message;
    }
    sayHello() {
        return "Hello, " + this.myMessage;
    }
}

Referencing the Hello Class in your code will produce hints and useful error messages if the correct type is not passed in the constructor.

TypeScript Playground
Of course all the good things come to an end.

When I started to add typings to more complicated code bases I quickly realised that I had to be smarter with my types. If I wanted to truly take advantage of the type system I would have to dig deeper and find better patterns.

I struggled the most with functions.

In this article I’ll try to demonstrate an incremental pattern that helped me to understand better some concepts in TypeScript.

Scenario: safeDelete()

We’ve been requested to write a function that takes 2 parameters: an Object and a key.

What this function should do is to delete the given key and return a copy of the new Object. If the key is not in the Object it should return undefined.

Plain JS version

function safeDelete(obj, key) {
    if (obj.hasOwnProperty(key)) {
        const shallowCopy = {...obj}
        delete shallowCopy[key];
        return shallowCopy;
    }
    return undefined;
}

// Calling the function
safeDelete({ a: 1, b: 2 }, 'b'); // {a: 1}

Let’s add some types to it, shall we?

TypeScript version (Step 1)

function safeDelete(obj: any, key: string) {
    if (obj.hasOwnProperty(key)) {
        const shallowCopy = {...obj}
        delete shallowCopy[key];
        return shallowCopy;
    }
    return undefined;
}

Simple enough right?

The problem with this approach is that the inferred return type is any

function safeDelete(obj: any, key: string): any

Let’s improve it!

TypeScript version (Step 2)

Let’s try to add a better type signature to our function

type objDelete = (obj: object, key: string) => object | undefined;

const safeDelete: objDelete = (obj, key) => {
      if (obj.hasOwnProperty(key)) {
        const shallowCopy = {...obj}
        delete shallowCopy[key as keyof typeof obj];
        return shallowCopy;
    }
    return undefined;
}

What we’re saying here is that our function will take only an object and a string as parameters and will return either undefined or an Object

The inferred return type will be:

const safeDelete: (obj: object, key: string) => object | undefined

TypeScript version (Step 3)

function safeDelete<T>(obj: T, key: string) {
    if (obj.hasOwnProperty(key)) {
        const shallowCopy = {...obj}
        delete shallowCopy[key as keyof typeof obj];
        return shallowCopy;
    }
    return undefined;
}

The inferred return type will be:

function safeDelete<{ a: number; b: number; }>(obj: { a: number; b: number; }, key: string): { a: number; b: number; } | undefined

Much better now! 👍

Using TypeScript Generics will allow us to create a component that can work over a variety of types rather than a single one.

The compile is super smart and will do the work for us.

You can now safely operate on the return value of the function.

As you can see you can progressively making your type signature smarter.

Top comments (0)