-
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
192: Uppercase, Lowercase, Capitalize, Uncapitalize
You've likely run into a situation where you have a set of string constants—maybe API endpoints, event names, or CSS property keys—and you need a derived version of those strings in a different case. I used to handle this by just writing out a second union type. It seems harmless at first, but it's a maintenance trap waiting to spring.
The manual maintenance trap
Let's say you're building a small event system. You have a list of events, and you want to define a corresponding set of handler names that are simply the capitalized versions of those events. The naive approach looks like this:
type UserEvent = 'login' | 'logout' | 'updateProfile';
type UserEventHandler = 'Login' | 'Logout' | 'UpdateProfile';
function handleEvent(event: UserEvent, handler: UserEventHandler) {
// ... logic here
}
On the surface, this works. But here is where it breaks: the moment your team decides to add a 'resetPassword' event, you have to remember to update UserEventHandler as well. If you forget, the compiler won't tell you that the two types are out of sync; it'll just tell you that 'ResetPassword' isn't allowed in the handler type. I've spent way too many hours debugging "undefined is not a function" errors because someone added a string to one union and forgot the other.
Letting the compiler do the heavy lifting
TypeScript provides intrinsic string manipulation types—Uppercase, Lowercase, Capitalize, and Uncapitalize—specifically to kill this pattern. These aren't functions that run at runtime; they are type-level transformations. Instead of duplicating your work, you can derive the handler type directly from the event type.
type UserEvent = 'login' | 'logout' | 'updateProfile';
// Now, if UserEvent grows, UserEventHandler grows with it automatically.
type UserEventHandler = Capitalize<UserEvent>;
// Result: 'Login' | 'Logout' | 'UpdateProfile'
This is a huge win for type safety. You've created a single source of truth. If you change 'login' to 'signIn', the handler type instantly updates to 'SignIn' without you touching a second line of code.
Choosing the right transformation
Depending on what you're building, you'll need different tools. Uppercase<T> and Lowercase<T> are blunt instruments—they hit every character. Capitalize<T> and Uncapitalize<T> only touch the first character. I find these particularly useful when working with naming conventions, like converting a PascalCase class name into a camelCase instance property.
type ComponentName = 'UserDashboard' | 'SettingsPanel';
type ComponentInstance = Uncapitalize<ComponentName>;
// Result: 'userDashboard' | 'settingsPanel'
One thing to keep in mind: these only work on string literal types. If you try to pass a generic string type into Capitalize, you just get string back. That's because TypeScript can't predict the casing of a value that hasn't been defined yet. These tools are for when you know the exact set of possible strings you're dealing with.
📋 Practical Task
Refactoring the CSS Property Mapper
You are working on a CSS-in-JS library. Currently, the library defines a set of supported CSS properties in kebab-case, and then manually defines a second type for the corresponding camelCase properties used in JavaScript. This is causing bugs whenever a new property is added.
Your Task: Refactor the following code to remove the manual JsCssProperty union. Use the Uncapitalize intrinsic type to derive JsCssProperty from CssProperty. Then, add 'border-radius' to the CssProperty union and verify that the derived type updates automatically.
type CssProperty = 'BackgroundColor' | 'FontSize' | 'MarginTop';
// TODO: Refactor this line to use Uncapitalize<CssProperty>
type JsCssProperty = 'backgroundColor' | 'fontSize' | 'marginTop';
function setStyle(prop: JsCssProperty, value: string) {
console.log(`Setting ${prop} to ${value}`);
}
// This should work automatically after your refactor
setStyle('borderRadius', '10px');
There are no comments for now.