-
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
35: Utility Types: ReturnType and Parameters
I was working on a project recently where I had a few complex "service" functions that handled API calls. The problem is that these functions often evolve. I'd add a field to the response object or change a parameter, and then I'd spend half my morning hunting down every single place where I'd manually typed that response shape just to fix a type error.
The Maintenance Headache
Let's look at a typical scenario. Imagine we have a function that fetches a user's configuration. I've let TypeScript infer the return type because the object is a bit nested and annoying to write out manually:
function getUserConfig(userId: string) {
return {
id: userId,
settings: {
theme: 'dark',
notifications: true,
language: 'en-US'
},
lastLogin: new Date()
};
}
Now, here's where it gets annoying. I want to write a "mock" version of this function for my tests. My first instinct is to just define a type for the return value:
type UserConfig = {
id: string;
settings: { theme: string; notifications: boolean; language: string };
lastLogin: Date;
};
const mockGetUserConfig = (userId: string): UserConfig => {
return {
id: userId,
settings: { theme: 'light', notifications: false, language: 'fr-FR' },
lastLogin: new Date()
};
};
This works fine today. But what happens if I add a timezone field to the real getUserConfig? I now have to remember to update the UserConfig type manually, or my mock will be out of sync with the real implementation. That's a recipe for bugs that only show up at runtime.
Extracting the Return Type
I wondered if there was a way to tell TypeScript: "Just look at getUserConfig and tell me what it returns." That's where ReturnType comes in. Now, if I try ReturnType<getUserConfig>, TypeScript screams at me. It wants a type, but getUserConfig is a value (the function itself).
To fix this, I have to use the typeof operator first to get the type signature of the function:
type ConfigReturn = ReturnType<typeof getUserConfig>;
// Now I can use ConfigReturn for my mock!
const mockGetUserConfig = (userId: string): ConfigReturn => {
return {
id: userId,
settings: { theme: 'light', notifications: false, language: 'fr-FR' },
lastLogin: new Date()
};
};
This is much cleaner. If I add a field to the original function, ConfigReturn updates automatically. I'm no longer duplicating the truth.
Figuring Out the Inputs
But we can take this further. What if I want to create a higher-order function—something that wraps getUserConfig to add logging or timing? I don't want to manually define the arguments for the wrapper either, because if I change userId: string to userId: number, I don't want to hunt through my wrappers to fix the types.
I tried using ReturnType, but obviously, that's for the output. I need the inputs. That's where Parameters comes in. Let's see what happens when we use it:
type ConfigParams = Parameters<typeof getUserConfig>;
// ConfigParams is actually [userId: string]
Notice that Parameters returns a tuple. This makes sense because functions can have multiple arguments. If I have a function with three arguments, Parameters gives me an array-like type with three elements. This is incredibly useful when combined with the spread operator.
Putting it All Together
Let's build a generic withLogging wrapper. I want this to work for any function, regardless of what it takes or returns. I'll use generics combined with our two new utility types:
function withLogging<T extends (...args: any[]) => any>(fn: T) {
return (...args: Parameters<T>): ReturnType<T> => {
console.log(`Calling function with args:`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const loggedGetUserConfig = withLogging(getUserConfig);
// TypeScript knows exactly that:
// 1. It takes a string (from Parameters)
// 2. It returns the Config object (from ReturnType)
const config = loggedGetUserConfig('user-123');
I love this pattern because it's completely decoupled. I can pass any function into withLogging, and the resulting function maintains the exact type safety of the original. No any, no manual interface updates, just pure type extraction.
📋 Practical Task
Exercise: Implementing a Type-Safe Event Dispatcher Mock
You are building a testing utility for an event system. You have a core function called dispatchEvent that handles sending events to a server. You need to create a createMockDispatcher function that returns a mock version of the dispatcher, but it must perfectly mirror the types of the original.
Requirements:
- Use the provided
dispatchEventfunction as the source of truth. - Use
Parametersto ensure the mock function accepts the exact same arguments as the original. - Use
ReturnTypeto ensure the mock function returns the exact same type as the original. - The mock function should simply
console.logthe arguments and return a dummy value that matches the expected return type.
// The source of truth
function dispatchEvent(eventName: string, payload: object, priority: number) {
return {
success: true,
timestamp: Date.now(),
eventId: Math.random().toString(36)
};
}
// YOUR TASK:
// Create a function called createMockDispatcher that returns
// a function with the same Parameters and ReturnType as dispatchEvent.
function createMockDispatcher() {
// Implement here
}
const mock = createMockDispatcher();
// This call should be type-checked against the original dispatchEvent signature
mock('user_signup', { email: 'test@test.com' }, 1);
There are no comments for now.