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
76: Building a Typed CLI Tool
You might be tempted to think that since process.argv is already typed as string[] in Node.js, your CLI tool is "typed" the moment you start reading arguments. It feels like the work is already done for you.
Here is why that is a dangerous trap. Let's say you're building a tool to optimize images, and you expect a --quality flag. If you just grab the value from the array, you aren't actually working with a number; you're working with a string that looks like a number. If you use a type assertion like as number to force it, you're lying to the TypeScript compiler. The compiler will stop complaining, but your code will crash the second you try to perform a mathematical operation on that value at runtime.
The "process.argv is enough" fallacy
// This looks fine to TS, but it's a runtime disaster waiting to happen
const args = process.argv.slice(2);
const quality = args[1] as unknown as number;
// If the user passes "--quality 80", quality is actually the string "80"
// This will result in "8080" instead of 160
console.log(quality + 80);
In the example above, TypeScript is happy because we forced the type, but the reality of the CLI environment is that everything entering your program is a string. A truly typed CLI tool isn't about how you cast the input, but how you parse and validate it into a known schema before it ever hits your business logic.
Bridging the gap between raw strings and domain types
To do this right, you need a boundary layer. I usually reach for a library like commander or yargs, but the secret sauce is defining a strict interface for your parsed options and ensuring the parser actually adheres to it. You want to move from string[] to a Config object as early as possible.
Let's look at how I'd actually structure the image optimizer tool. I want a specific set of formats and a guaranteed number for quality.
import { program } from 'commander';
type ImageFormat = 'webp' | 'jpg' | 'png';
interface CliOptions {
source: string;
quality: number;
format: ImageFormat;
}
program
.requiredOption('-s, --source ', 'path to source image')
.option('-q, --quality ', 'compression quality (1-100)', (val) => parseInt(val, 10))
.option('-f, --format ', 'output format (webp, jpg, png)', 'webp')
.parse(process.argv);
// We cast the options once at the boundary.
// Because we used a parsing function for 'quality', it's actually a number now.
const options = program.opts() as unknown as CliOptions;
if (options.quality < 1 || options.quality > 100) {
console.error('Error: Quality must be between 1 and 100');
process.exit(1);
}
console.log(`Optimizing ${options.source} to ${options.format} at ${options.quality}%`);
Notice what happened here. I didn't just tell TypeScript "trust me, this is a number." I used the commander custom processing function (val) => parseInt(val, 10) to actually change the data type at runtime. The interface CliOptions then acts as the contract for the rest of my application. Now, if I try to pass options.quality into a function that expects a string, TypeScript will actually catch it—which is exactly what we want.
I've found that treating CLI input as "untrusted" until it passes through a validation layer saves me hours of debugging "undefined is not a function" errors that only happen when a user forgets a flag in production.
📋 Practical Task
Build a Typed Log-Filtering CLI
Create a CLI tool called log-filter that reads a log file and filters entries by a specific severity level. Your tool must implement the following requirements:
- Define a union type
Severity = 'INFO' | 'WARN' | 'ERROR'. - Define a
FilterOptionsinterface that includesfilePath(string) andlevel(Severity). - Use a CLI parser (like
commander) to handle the arguments. - Implement a custom parsing function for the
levelargument that converts the user's input to uppercase and validates that it is one of the three allowedSeverityvalues. If the value is invalid, the program should print an error and exit. - Ensure that once the options are parsed, the rest of your logic interacts only with the typed
FilterOptionsobject, not the rawprocess.argv.
There are no comments for now.