Skip to main content

Command Palette

Search for a command to run...

Typescript Utility Type - ReturnType

Updated
5 min read
Typescript Utility Type - ReturnType

Despite having a reputation of making developers' lives more complicated, Typescript does still provide a huge benefit if you use it well. One of the best ways to use it is to utilize utility types. They are built-in types that you can use to perform safe type-checking on your codebase. In this article, we'll explore one of the utility types - ReturnType.

What is ReturnType?

ReturnType allows you to easily create a type that represents whatever a function returns. Let's look at an example:

function f() {
  return { a: 1, b: 2, }
}

// F is now { a: 1, b: 2 } type
type F = ReturnType<typeof f>;

Pretty simple, right? Well, what's the use case of ReturnType? Let's now look at a simple use case for it.

Reduces Type Maintenance Cost

The power of ReturnType comes into play when we're invoking a same function multiple times in various locations and multiple other functions rely on the values returned by that function. Let's look at an example:

function getPosts() {
    return [
        {
            title: "Title 1",
            author: "Author 1",
            content: "Content 1",
        },
        {
            title: "Title 2",
            author: "Author 2",
            content: "Content 2",
        }, 
        {
            title: "Title 3",
            author: "Author 3",
            content: "Content 3",
        }
    ]
}

function removeContent(
    posts: {
        title: string;
        author: string;
        content: string;
    }[]
) {
    return posts.map(({content, ...rest}) => rest);
}

function addDisplayOrder(
    posts: {
        title: string;
        author: string;
        content: string;
    }[]
) {
    return posts.map((post, index) => ({ ...post, displayOrder: index }));
}

function combineTitleAuthor(
    posts: {
        title: string;
        author: string;
        content: string;
    }[]
) {
    return posts.map(({title, author, content}) => ({ titleAuthor: `${title} - ${author}`, content }));
}

function main() {
    const posts = getPosts();

    const postsWithoutContent = removeContent(posts);
    const postsWithDisplayOrder = addDisplayOrder(posts);
    const combinedTitleAuthorPosts = combineTitleAuthor(posts);

    // ...
}

What's "wrong" with this code? If you have keen eyes, you would've already spotted, but we actually have a repeating code, which is the type of posts used as an argument for removeContent, addDisplayOrder and combineTitleAuthor. So here's an improved code:

function getPosts() {
    return [
        {
            title: "Title 1",
            author: "Author 1",
            content: "Content 1",
        },
        {
            title: "Title 2",
            author: "Author 2",
            content: "Content 2",
        }, 
        {
            title: "Title 3",
            author: "Author 3",
            content: "Content 3",
        }
    ]
}

type Posts = {
    title: string;
    author: string;
    content: string;
}[];

function removeContent(posts: Posts) {
    return posts.map(({content, ...rest}) => rest);
}

function addDisplayOrder(posts: Posts) {
    return posts.map((post, index) => ({ ...post, displayOrder: index }));
}

function combineTitleAuthor(posts: Posts) {
    return posts.map(({title, author, content}) => ({ titleAuthor: `${title} - ${author}`, content }));
}

function main() {
    const posts = getPosts();

    const postsWithoutContent = removeContent(posts);
    const postsWithDisplayOrder = addDisplayOrder(posts);
    const combinedTitleAuthorPosts = combineTitleAuthor(posts);

    // ...
}

Much better! But what happens when we add number of views for each post? Let's take a look:

function getPosts() {
    return [
        {
            title: "Title 1",
            author: "Author 1",
            content: "Content 1",
            views: 1,
        },
        {
            title: "Title 2",
            author: "Author 2",
            content: "Content 2",
            views: 2,
        }, 
        {
            title: "Title 3",
            author: "Author 3",
            content: "Content 3",
            views: 3,
        }
    ]
}

type Posts = {
    title: string;
    author: string;
    content: string;
    // Need to add views field
    views: number;
}[];

function removeContent(posts: Posts) {
    return posts.map(({content, ...rest}) => rest);
}

function addDisplayOrder(posts: Posts) {
    return posts.map((post, index) => ({ ...post, displayOrder: index }));
}

function combineTitleAuthor(posts: Posts) {
    return posts.map(({title, author, content}) => ({ titleAuthor: `${title} - ${author}`, content }));
}

function main() {
    const posts = getPosts();

    const postsWithoutContent = removeContent(posts);
    const postsWithDisplayOrder = addDisplayOrder(posts);
    const combinedTitleAuthorPosts = combineTitleAuthor(posts);

    // ...
}

Once we added views field to each post, we had to add views: number; to Posts type. If you're used to construct a type this way, you may think that it's the best we can do. However, you can actually improve this further by utilizing ReturnType.

function getPosts() {
    return [
        {
            title: "Title 1",
            author: "Author 1",
            content: "Content 1",
            views: 1,
        },
        {
            title: "Title 2",
            author: "Author 2",
            content: "Content 2",
            views: 2,
        }, 
        {
            title: "Title 3",
            author: "Author 3",
            content: "Content 3",
            views: 3,
        }
    ]
}

type Posts = ReturnType<typeof getPosts>

function removeContent(posts: Posts) {
    return posts.map(({content, ...rest}) => rest);
}

function addDisplayOrder(posts: Posts) {
    return posts.map((post, index) => ({ ...post, displayOrder: index }));
}

function combineTitleAuthor(posts: Posts) {
    return posts.map(({title, author, content}) => ({ titleAuthor: `${title} - ${author}`, content }));
}

function main() {
    const posts = getPosts();

    const postsWithoutContent = removeContent(posts);
    const postsWithDisplayOrder = addDisplayOrder(posts);
    const combinedTitleAuthorPosts = combineTitleAuthor(posts);

    // ...
}

We've switched from using { title: string; author: string; content: string; views: number; }[] to ReturnType<typeof getPosts>. You might wonder why. Before, the Posts and the return type of getPosts function were not aligned, meaning any changes in getPosts' return type had to be manually updated in Posts. Now, with the current code, Posts and getPosts are in sync, ensuring that any changes in the return type of getPosts are automatically reflected in Posts. This might seem trivial, but it's quite beneficial, especially as your codebase grows. A larger codebase means longer type-checking times by your linter, and this synchronization reduces the need for additional iterations, which can be quite impactful.

Conclusion

Utilizing TypeScript's ReturnType simplifies code management by automatically syncing type definitions, reducing manual updates and maintenance overhead. This not only enhances consistency across the codebase but also streamlines development, particularly in large-scale projects. Happy hacking 👨‍💻!

More from this blog

// Sean's SWE Journey

19 posts

Love to learn, develop and share new ideas 👨‍💻