TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
17: Function Overloads
You've probably run into a situation where a function needs to be flexible. Maybe it accepts a string and returns a number, but if it accepts a number, it returns a string. Or perhaps it takes a single ID and returns a user object, but if it takes a configuration object, it returns a newly created user. In these cases, your first instinct is likely to reach for union types. It feels like the "TypeScript way."
The Union Type Trap
Let's look at a utility function I wrote a while back for a data processing tool. I wanted a function called formatValue that could handle either a raw number (returning it as a formatted string) or a formatted string (returning it as a number for calculations). Here is how I originally wrote it:
function formatValue(value: string | number): string | number {
if (typeof value === 'string') {
return parseFloat(value);
}
return value.toFixed(2);
}
On the surface, this looks fine. It compiles. But here is where the experience falls apart for the person calling the function. If I call formatValue("10.5"), I know for a fact that I'm getting a number back. However, TypeScript doesn't know that. It looks at the return signature and tells me the result is string | number. Now I'm forced to use a type guard or a clumsy type assertion (the dreaded as number) every single time I use the function, just to satisfy the compiler.
This is the core problem with simple unions: they describe the inputs and outputs in isolation. They don't describe the relationship between a specific input and a specific output.
Tying Inputs to Outputs with Overloads
This is exactly why function overloads exist. Overloading allows you to tell TypeScript: "If the input is X, the output will definitely be Y. But if the input is A, the output will be B."
To do this, you write one or more "overload signatures" first, followed by the actual "implementation signature." The implementation signature is the one that actually contains the function body, and it's not visible to the caller—only the overloads are.
// Overload signatures
function formatValue(value: string): number;
function formatValue(value: number): string;
// Implementation signature
function formatValue(value: string | number): string | number {
if (typeof value === 'string') {
return parseFloat(value);
}
return value.toFixed(2);
}
Now, when you call formatValue("10.5"), TypeScript matches it against the first overload and knows the return type is number. No more casting, no more ambiguity. It's a much cleaner experience for whoever is using your API.
The Implementation Gap
There is one detail here that trips people up: the implementation signature must be compatible with all of the overloads. I've seen developers try to make the implementation signature too specific, but it has to be broad enough to cover every single overload you've defined. If you have an overload that accepts a boolean but your implementation only accepts string | number, TypeScript will throw an error.
I should also mention a common mistake: don't use overloads just to avoid writing a more generic type. If you find yourself writing five overloads that all do the same thing, you're probably better off using a Generic. Overloads are for when the return type changes based on the type of the argument, not just when the types are different.
📋 Practical Task
Refactoring the API Response Handler
You are working on a library that fetches data from an API. Currently, there is a function called fetchData that is causing headaches for the team because it uses a union return type, forcing everyone to use type assertions.
The Current Code:
interface User { id: string; name: string; }
interface Post { id: string; title: string; }
function fetchData(id: string, type: 'user' | 'post'): User | Post {
if (type === 'user') {
return { id, name: 'John Doe' };
}
return { id, title: 'Hello World' };
}
// This is causing the issue:
const user = fetchData('123', 'user');
// 'user' is inferred as User | Post, but we know it's a User!
Your Task:
Rewrite the fetchData function using function overloads so that when 'user' is passed as the type, the return type is strictly User, and when 'post' is passed, the return type is strictly Post. Ensure the implementation signature remains compatible with both overloads.
There are no comments for now.