Smarter Return Types in TypeScript: A Developer’s Guide

Smarter Return Types in TypeScript: A Developer’s Guide
Photo by Sigmund / Unsplash

As our TypeScript codebases grow, clarity and consistency become more important than clever tricks. One underrated but impactful practice is how we define return types—or more accurately, when we should and when we shouldn't.

This article walks through my current recommendation (yes, it's changed a few times), and highlights some useful considerations that you might have overlooked.


🎯 Always Explicit for Top-Level Functions

When you're writing a top-level function—especially one that’s exported or reused across multiple files—always declare the return type. This isn’t just for TypeScript. It’s for:

  • Human readers who want to understand what your function is supposed to do,
  • Static analysis tools and AI assistants that can offer better support,
  • Future-you, when you're trying to figure out why something is returning a weird value.
// ✅ Good
const getGreeting = (): string => {
  return "Hello";
};

Even though TypeScript can infer this, inference isn't documentation. Explicit return types act as contracts.


🎨 Skip It for JSX Components

There’s one clear exception: React components. If your function is a component returning JSX, don't bother specifying the return type.

// ✅ Also Good
const HelloWorld = () => {
  return <div>Hello World</div>;
};

In this case, the return type is always something React can render—JSX or ReactNode. Adding an explicit return type like JSX.Element or React.ReactElement often adds noise without much benefit.

Let the code speak for itself.


🤝 Helper Functions Inside Components? Declare It.

While we can skip return types for components, any helper function inside a component that performs logic (e.g., formatting data, calculations) should still declare its return type.

const UserCard = ({ name }: { name: string }) => {
  const formatName = (name: string): string => {
    return name.trim().toUpperCase();
  };

  return <div>{formatName(name)}</div>;
};

This keeps the function self-describing and avoids hidden surprises, especially when logic becomes more complex over time.


🧪 Don’t Trust TypeScript’s Inference Blindly

Yes, TypeScript is smart. But relying too much on inference:

  • Makes it harder to detect accidental return changes
  • Reduces IDE readability (you need to hover or peek definitions)
  • Can lead to wider-than-necessary return types (e.g., string | undefined when you expected string)
// ❌ Might return undefined, but you won't know unless you dig
const getName = (user: User) => user.name;

Be intentional. If you expect a string, say it:

const getName = (user: User): string => user.name ?? "Anonymous";

📦 Use void or Promise<void> for Side-Effect Functions

If your function performs a side effect and doesn't return anything meaningful, use void (or Promise<void> for async).

const logMessage = (msg: string): void => {
  console.log(msg);
};

const syncData = async (): Promise<void> => {
  await someApiCall();
};

This avoids false assumptions that the function returns something usable.


🧼 Clean Code Is Clear Code

Here’s the bottom line:

  • Use explicit return types for top-level and utility functions.
  • Skip them for JSX-returning components unless there's an edge case.
  • Be mindful of inferred types, and tighten them when necessary.
  • Use void to clearly signal non-returning behavior.
  • Consider your audience—teammates, future maintainers, and tools—when writing functions.

By being intentional about return types, you improve not just correctness, but the readability, reliability, and maintainability of your code.

Support Us