-
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
144: Tuple Types with Optional and Rest Elements
I've been working on a small telemetry system for a game project, and I hit a wall with how I was defining my data packets. I wanted a structure that was more rigid than a plain array, but more flexible than a standard fixed-length tuple. Let's walk through how I solved this using optional and rest elements.
The rigidity of basic tuples
Initially, I thought a simple tuple would do the trick for a log entry. I wanted the first element to be a timestamp (a number) and the second to be the event name (a string). It looked like this:
type LogEntry = [number, string];
const entry1: LogEntry = [1625097600, "PlayerJoined"]; // Works great.
const entry2: LogEntry = [1625097601]; // Error: Source has 1 element but target requires 2.
const entry3: LogEntry = [1625097602, "PlayerLeft", "ID_42"]; // Error: Source has 3 elements but target allows only 2.
This is too restrictive. Sometimes my logs don't have an event name (maybe it's just a heartbeat ping), and other times I need to attach a bunch of extra metadata strings at the end. If I just used (number | string)[], I'd lose the guarantee that the first element is always the timestamp. That's a trade-off I'm not willing to make.
Adding a bit of breathing room
I wondered if I could make that event name optional. In TypeScript, you can apply the ? modifier to tuple elements, just like you do with object properties. Let's try that:
type LogEntry = [number, string?];
const entry1: LogEntry = [1625097600, "PlayerJoined"]; // Still works.
const entry2: LogEntry = [1625097601]; // Now this works!
That solves the "heartbeat" problem. But I'm still stuck on the metadata. If I try to add a third element, TypeScript still yells at me because the tuple length is capped at two. I need a way to say, "Start with these specific things, but then take as many strings as you want."
Opening the floodgates with rest elements
This is where rest elements come in. By using the spread operator ... inside the tuple definition, I can tell TypeScript that the remainder of the array should follow a specific type. I'll try adding a rest element of strings at the end:
type LogEntry = [number, string, ...string[]];
const entry1: LogEntry = [1625097600, "PlayerJoined", "Region: US-East", "Latency: 40ms"];
// This works perfectly.
Wait, I just realized I broke my heartbeat logs again. Because the second element string is required, [1625097601] is now invalid again. The rest element allows for extra items, but it doesn't make the preceding required items optional.
The "Goldilocks" Tuple
To get exactly what I want, I need to combine both concepts. I want a required timestamp, an optional event name, and then an optional, indefinite list of metadata tags. Let's see if this combination holds up:
type LogEntry = [number, string?, ...string[]];
const heartbeat: LogEntry = [1625097605];
// Valid: timestamp present, name missing, rest empty.
const simpleEvent: LogEntry = [1625097606, "PlayerJumped"];
// Valid: timestamp and name present, rest empty.
const detailedEvent: LogEntry = [1625097607, "ItemPickedUp", "Item: Sword", "Slot: 1", "Rarity: Epic"];
// Valid: timestamp, name, and three metadata strings.
This is the sweet spot. I've maintained the type safety of the first element while allowing the rest of the data to be as fluid as a regular array. One thing to keep in mind: when you use a rest element, it must be the last element in the tuple. You can't put a required element after a rest element because TypeScript wouldn't know where the "rest" ends and the final required element begins.
📋 Practical Task
Build a Multi-Purpose Command Parser
You are building a CLI tool that processes commands. Each command is passed as an array (a tuple). The requirements are as follows:
- The first element must always be a
string(the command name, e.g., "git", "npm"). - The second element should be an optional
boolean(a "verbose" flag). - The remaining elements should be an indefinite number of
stringarguments.
Your Task:
- Define a tuple type called
CommandInputthat implements these rules. - Create three different variables using this type:
cmd1: A command with only a name.cmd2: A command with a name and the verbose flag set to true.cmd3: A command with a name, the verbose flag set to false, and three string arguments.
There are no comments for now.