-
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
225: Whiteboard Practice: Implementing a Type-Safe Curry Function
Currying is one of those functional programming concepts that sounds intimidating until you actually see it in action. At its core, it's just transforming a function that takes multiple arguments into a series of functions that each take a single argument. In JavaScript, it's trivial. In TypeScript, making it type-safe—meaning the compiler knows exactly which argument is expected at each step—is where the real challenge lies.
Let's pretend we're at a whiteboard. I want to build a utility for a pricing engine. I have a function that calculates a final price based on a tax rate, a discount, and the base price. It looks like this:
function calculateTotal(tax: number, discount: number, price: number) {
return price * (1 - discount) * (1 + tax);
}
Now, in a real app, I might know the tax rate and discount for a specific region, but not the price of the item yet. I want to "preset" those first two arguments. That's where a curry function comes in.
The "Quick and Dirty" Attempt
If I were writing this in plain JS, I'd just write a wrapper. Let's try that in TS and see where it breaks.
function curry(fn: Function) {
return function curried(...args: any[]) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return (...nextArgs: any[]) => curried(...args, ...nextArgs);
}
};
}
const curriedTotal = curry(calculateTotal);
const applyVAT = curriedTotal(0.20); // Tax is 20%
const applyVATAndDiscount = applyVAT(0.10); // Discount is 10%
const finalPrice = applyVATAndDiscount(100); // Price is 100
This works at runtime, but look at the types. If I hover over finalPrice, it's any. If I accidentally pass a string to applyVAT, TypeScript won't complain. We've essentially thrown away the type system to get the functionality. That's a fail in my book.
Peeling the Parameter Onion
To fix this, we need a type that can "peel off" arguments from the original function one by one. We can use conditional types and the infer keyword to look at the function's parameters as a tuple.
I'll try to define a type Curried that checks: "Is there a first argument? If so, return a function that takes that argument and returns another Curried version of the remaining arguments."
type Curried<Args extends any[], Return> =
Args extends [infer First, ...infer Rest]
? (arg: First) => Curried<Rest, Return>
: Return;
I like this logic. It's recursive. It keeps slicing the Args tuple until there's nothing left, at which point it finally returns the Return type. Now let's try to plug this into our curry function. This is where it gets a bit fiddly with generics.
Connecting the Logic to the Implementation
I need to capture the parameters of the input function F. TypeScript has a built-in utility called Parameters<T> that does exactly that. Let's see if we can marry the recursive type with the actual function implementation.
function curry<F extends (...args: any[]) => any>(fn: F): Curried<Parameters<F>, ReturnType<F>> {
return function curried(...args: any[]) {
if (args.length >= fn.length) {
return fn(...args) as any;
} else {
return (...nextArgs: any[]) => curried(...args, ...nextArgs) as any;
}
} as any;
}
You'll notice I used a few as any casts inside the implementation. I'll be honest with you: when you're writing highly generic recursive types in TypeScript, the compiler often struggles to prove that the recursive implementation perfectly matches the recursive type. As long as the public API (the function signature) is type-safe, a few internal casts are usually a necessary evil in utility libraries.
Testing the Safety Net
Now let's see if the compiler actually helps us. If I try to use our curriedTotal now:
const curriedTotal = curry(calculateTotal);
const applyVAT = curriedTotal(0.20); // Correct: expects number
const error = applyVAT("high"); // TypeScript Error: Argument of type 'string' is not assignable to parameter of type 'number'.
There it is. The compiler knows that the second call to applyVAT must be a number because it's looking at the second element of the calculateTotal parameter list. We've successfully turned a runtime convenience into a compile-time guarantee.
📋 Practical Task
Exercise: Build a Type-Safe Curried Logger
In this exercise, you will implement a curried logging function. You are provided with a base function logMessage(level: string, component: string, message: string). Your goal is to use the curry implementation from the lesson to create a specialized logger.
Requirements:
- Implement the
Curriedtype and thecurryfunction as explored in the lesson. - Create a curried version of
logMessage. - Create a specialized function called
logNetworkErrorby pre-filling thelevelas"ERROR"and thecomponentas"NetworkModule". - Verify that calling
logNetworkError("Timeout occurred")works and is type-safe, while callinglogNetworkError(404)produces a TypeScript error.
// Base function to curry
function logMessage(level: string, component: string, message: string) {
console.log(`[${level}] [${component}]: ${message}`);
}
// Your implementation of Curried type and curry function goes here...
// Your specialized logger implementation goes here...
There are no comments for now.