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
62: Variadic Tuple Types
You've likely dealt with generics and arrays in TypeScript, but there's a specific point where standard arrays stop being useful: when the order and number of elements matter. That's where tuples come in. But for a long time, tuples were rigid. If you wanted to create a function that manipulated the arguments of another function—like adding a logger or a session ID to the front of the argument list—you usually ended up falling back to any[] and losing all your type safety.
I see this all the time in middleware or wrapper functions. Let's look at a piece of code that looks correct at a glance but completely fails to provide the safety we expect from TypeScript.
The Type Erosion Problem
type Handler = (id: number, name: string) => void;
const saveUser: Handler = (id, name) => {
console.log(`Saving user ${name} with ID ${id}`);
};
// We want a wrapper that adds a "traceId" to every call
function wrapWithTrace(fn: (...args: any[]) => void) {
return (...args: any[]) => {
const traceId = Math.random().toString();
console.log(`Trace: ${traceId}`);
// This is where the danger is: we're just spreading 'any'
fn(...args);
};
}
const wrappedSave = wrapWithTrace(saveUser);
// BUG: TypeScript doesn't complain about this, but it will crash or behave
// unexpectedly at runtime because the arguments are wrong.
wrappedSave("wrong", 123, "too many arguments");
The problem here is "type erosion." By using any[], we've told TypeScript to stop checking the arguments. The wrappedSave function now accepts literally anything. Even worse, if we tried to use a generic T extends any[] without variadic tuple types, we'd struggle to define a return type that precisely describes the new argument list (which should be the original list plus the trace ID).
Preserving Structure with Variadic Tuple Types
Variadic Tuple Types allow us to use the spread operator ... inside a tuple type definition. This lets us say: "This tuple starts with X, and then contains whatever was in that other tuple."
Here is how I would refactor that wrapper to ensure the compiler knows exactly what the wrapped function requires, while still allowing the wrapper to add its own arguments.
type Handler = (id: number, name: string) => void;
const saveUser: Handler = (id, name) => {
console.log(`Saving user ${name} with ID ${id}`);
};
// We use T to capture the exact tuple of arguments the original function expects
function wrapWithTrace<T extends any[]>(fn: (...args: T) => void) {
// We define the new argument list as a tuple:
// The first element is a string (our traceId), followed by the original tuple T
return (...args: [string, ...T]): void => {
const [traceId, ...rest] = args;
console.log(`Trace: ${traceId}`);
// Because 'rest' is typed as T, this call is perfectly safe
fn(...rest);
};
}
const wrappedSave = wrapWithTrace(saveUser);
// Now, TypeScript correctly identifies that the first arg must be a string,
// followed by the Handler's arguments: (string, number, string)
wrappedSave("req-123", 1, "Alice"); // Works!
// This will now throw a compile-time error:
// Argument of type 'string' is not assignable to parameter of type 'number'.
wrappedSave("req-123", "wrong", "Alice");
What happened here is that [string, ...T] created a new, dynamic tuple. TypeScript doesn't just see "an array of things"; it sees a sequence where the first element is fixed as a string and the subsequent elements are mapped exactly to the signature of saveUser. This is the "magic" of variadic tuple types—they let us perform algebra on the shapes of our functions.
I should mention that this isn't just for adding elements to the front. You can use ...T to slice tuples, merge two different tuples together, or create "rest" parameters that maintain their specific types instead of collapsing into a general array. It's the difference between telling TypeScript "there are some numbers here" and "there is exactly one string, followed by exactly two numbers, followed by whatever else was in the original list."
📋 Practical Task
Build a Type-Safe Command Dispatcher
You are building a command system where different commands take different arguments. You need to create a dispatch function that acts as a gateway. The dispatch function should take the command name as the first argument, and then the exact arguments required by that specific command.
Your Task:
- Create a
CommandMapinterface where keys are command names (e.g.,"move","jump") and values are the functions that implement those commands. - Implement a
dispatchfunction using variadic tuple types. It should take the command name and a spread of arguments. - Ensure that if you call
dispatch("move", 10, 20)and the "move" command expects two numbers, it passes. If you pass a string instead, TypeScript should throw an error.
Hint: You'll need to use a generic that extends the keys of your CommandMap and then use the spread operator to link the arguments of the chosen command to the arguments of the dispatch function.
There are no comments for now.