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
85: Writing a Simple TypeScript Transformer
Wait, how do I actually get "inside" the compiler to change code?
You can't just use a regex to find and replace strings if you want this to be reliable. To write a transformer, you have to work with the Abstract Syntax Tree (AST). Think of the AST as a giant nested object that represents the structure of your code. TypeScript provides a "visitor" pattern to navigate this. You write a function that "visits" every node in the tree, checks if it's something you care about, and then either returns the node as-is or returns a modified version of it.
Here is the basic skeleton of a transformer. I usually start with a function that returns another function—the actual transformation logic:
import * as ts from 'typescript';
const transformer: ts.Transformer = (context) => {
return (rootNode) => {
const visit = (node: ts.Node): ts.Node => {
// This is where the magic happens
return ts.visitEachChild(node, visit, context);
};
return ts.visitNode(rootNode, visit);
};
};
The ts.visitEachChild call is critical. Without it, you're only looking at the root of your file and ignoring everything inside functions, classes, or loops. It's a recursive descent; you're essentially telling TypeScript, "Keep digging deeper until you hit the leaves of the tree."
How do I target a specific function call, like a debug() helper?
Let's say you have a debug('message') function used everywhere in your app, but you want it completely stripped out of the production build to save on bundle size. You don't want it to just be an empty function; you want the call to vanish entirely.
To do this, you look for ts.SyntaxKind.CallExpression. But since there are thousands of function calls in a project, you have to verify that the function being called is actually named "debug".
const visit = (node: ts.Node): ts.Node => {
if (ts.isCallExpression(node)) {
// Check if the expression being called is an identifier named 'debug'
if (ts.isIdentifier(node.expression) && node.expression.text === 'debug') {
console.log('Found a debug call! Removing it...');
return undefined; // Returning undefined effectively deletes the node from the AST
}
}
return ts.visitEachChild(node, visit, context);
};
I've found that using the type guards like ts.isCallExpression is much cleaner than manually checking node.kind === ts.SyntaxKind.CallExpression. It gives you better type narrowing inside the if block, so you don't have to cast types constantly.
Can I replace code with something else, or just delete it?
Deleting is easy (just return undefined), but replacing is where transformers get powerful. Suppose instead of deleting debug(), you want to replace it with console.log() during development. You can't just return a string; you have to construct a new AST node using the ts.factory API.
It feels a bit verbose at first because you're essentially building the code manually, but it's the only way to ensure the resulting JavaScript is syntactically correct.
if (ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'debug') {
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('console'),
ts.factory.createIdentifier('log')
),
undefined,
node.arguments // Keep the original arguments passed to debug()
);
}
Just a word of warning: be careful with scope. If you replace a variable name, you aren't checking if that variable is actually defined in the current scope. You're operating on the structure of the code, not necessarily the semantic meaning. If you start doing heavy lifting with variable renaming, you'll need to use the ts.TypeChecker, which is a whole other beast.
How do I actually run this? tsc doesn't have a --transformer flag.
You've probably noticed this by now. The standard tsc compiler is intentionally closed-off; it doesn't let you plug in custom transformers via a config file. This is one of the more frustrating parts of the TS ecosystem.
In a real-world professional setup, you have two main options:
- ttypescript: This is a wrapper around
tscthat allows you to specify transformers in yourtsconfig.json. It's the "classic" way to do it. - Custom Build Script: You can write a small Node.js script that uses the TypeScript Compiler API to read files, apply your transformer, and write the output to disk.
If you're using Webpack or Vite, they usually have plugins (like ts-loader) that provide a hook to inject these transformers during the transpilation process. I usually prefer the loader approach because it integrates directly into the hot-reload cycle.
📋 Practical Task
Exercise: The "Telemetry Stripper" Transformer
Your goal is to build a TypeScript transformer that identifies all calls to a function named telemetry.send() and replaces them with a no-op (removes them entirely) to prevent tracking data from being sent in a local development environment.
Requirements:
- The transformer must specifically target
CallExpressions. - It must check if the call is a
PropertyAccessExpressionwhere the object istelemetryand the property issend. - The node should be removed from the AST (return
undefined). - Ensure that other calls to
telemetry(e.g.,telemetry.init()) are left untouched.
Starter Code Snippet:
// Target this code:
telemetry.send("user_login"); // Should be removed
telemetry.init(); // Should remain
console.log("App started"); // Should remainThere are no comments for now.