TypeScript isn't just about guarding runtime bugs; it's a toolkit for cleaner, safer code with less boilerplate. A handful of built-in aliases, utility types, and subtle syntax tricks can dramatically reduce repetition and the chance of subtle errors. In this post, I'll share practical TypeScript gems you're likely to overlook—short hands, aliases, and underutilized built-ins you can drop into real projects today.
TL;DR
- Leverage compact utility types and type aliases (including branded types) to cut boilerplate and prevent mix-ups
- Use mapped types, conditional types, and
as constto create ergonomic, expressive APIs - Tap into built-ins like
Awaited, Template Literal Types, and discriminated unions for safer, clearer code - See concrete patterns: deep partial updates, branded IDs, deep readonly configurations, and discriminated action handling
- Each example includes a minimal, runnable snippet you can copy-paste into your project
Prerequisites
- TypeScript 4.1+ for template literal types; 4.5+ for
Awaited; 4.x+ generally fine for most features here - Basic familiarity with TypeScript types, interfaces, generics
- A node/tsconfig project to try snippets locally (optional: a small repo you can clone to test)
What You'll Learn
-
Practical type tricks:
DeepPartial,DeepReadonly,Mutable, branded IDs -
Short hands and ergonomics:
as const,inferin conditional types,ReturnType/Parameters -
Useful built-ins you may not fully leverage:
Awaited, Template Literal Types, discriminated unions, Opaque/branding patterns - Real-world patterns: safe config loading, typed IDs, ergonomic API shapes, and robust error handling
Section 1: Utility Types You Might Be Overlooking
DeepPartial: Partial Structures at All Nesting Levels
Need to update nested objects without specifying every field? DeepPartial makes all properties optional recursively:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
};
// Usage
type User = {
id: string;
profile: {
name: string;
bio?: string;
};
roles: string[];
};
type UpdateUser = DeepPartial<User>;
// Accepts:
// { profile: { name?: string }, roles?: string[] }
NonNullable in Practice
Extract non-nullable values to ensure type safety:
type SafeProp<T, K extends keyof T> = T[K] extends null | undefined ? never : T[K];
// Example constraint
function getName<T extends { name?: string | null }>(obj: T): string {
return obj.name ?? "anonymous";
}
Section 2: Type Aliases and Branded Types
Branded/Opaque IDs to Prevent Mixing IDs
Ever passed the wrong ID to a function? Branded types prevent this at compile time:
type UserId = string & { __brand?: "UserId" };
type OrderId = string & { __brand?: "OrderId" };
function asUserId(id: string): UserId {
return id as UserId;
}
function getUser(userId: UserId) {
// runtime fetch by id
}
Benefit: Runtime value remains a string, but the compiler enforces correct ID usage.
When to Use Type Aliases vs Interfaces
Use interfaces for public API shapes and object literals you intend to extend; aliases for unions, primitives, or branded types.
Section 3: Mapped Types and Conditional Tricks
Mutable Pattern
Remove readonly modifiers when you need mutability:
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
type ReadonlyPoint = Readonly<{ x: number; y: number }>;
type Point = Mutable<ReadonlyPoint>;
const p: Point = { x: 1, y: 2 };
p.x = 3; // ✅ allowed
DeepReadonly (Read-Only Everywhere)
Lock down entire configuration objects:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
type Config = {
server: { host: string; port: number };
features: string[];
};
type SafeConfig = DeepReadonly<Config>;
Distributive Conditional Tricks
type IsStringLike<T> = T extends string ? true : false;
type A = IsStringLike<string | number>; // true | false
Section 4: Short Hands and Ergonomic Syntax
as const for Literal Narrowing and Discriminated Unions
const FETCH_USERS = { type: "FETCH_USERS" } as const;
type Action = typeof FETCH_USERS;
type Response =
| { status: "ok"; data: string[] }
| { status: "error"; error: string };
function handle(res: Response) {
if (res.status === "ok") {
// res.data is string[]
}
}
infer in Conditional Types
Extract types from arrays or other generic structures:
type ElementType<T> = T extends (infer U)[] ? U : T;
type T1 = ElementType<string[]>; // string
type T2 = ElementType<number>; // number
Awaited for Unwrapping Promises
async function fetchData(): Promise<{ ok: boolean }> {
return { ok: true };
}
type Data = Awaited<ReturnType<typeof fetchData>>; // { ok: boolean }
Template Literal Types for Chemistry Between Strings and Types
type RoutePath = `/users/${string}` | `/projects/${string}`;
function navigate(path: RoutePath) {
// runtime navigation
}
Section 5: Built-ins You May Not Fully Leverage
-
Opaque branding via intersection (as shown with
UserId/OrderId) - Template literal types to encode string shapes in types
- Yielding with Generator types (advanced exercise)
-
AsyncReturnType pattern (a predecessor to
Awaitedin 4.x)
Note: Some of these require careful design to avoid over-engineering—use where they solve a real pain point.
Section 6: Practical Patterns with Concrete, Minimal Examples
Example 1: Safe Configuration Loader
type Config = {
port?: number;
host?: string;
};
type RequiredConfig = Required<Config>;
function loadConfig(input: Partial<Config>): Config {
const defaultConfig: Config = { port: 3000, host: "localhost" };
return { ...defaultConfig, ...input };
}
// Usage
const cfg = loadConfig({ host: "example.com" });
Example 2: Discriminated Union for API Responses
type ApiResponse =
| { ok: true; data: string[] }
| { ok: false; error: string };
function handleResponse(r: ApiResponse) {
if (r.ok) {
// r.data is string[]
console.log("Got data", r.data);
} else {
console.error("API error", r.error);
}
}
Example 3: Function Ergonomics with Generics and ReturnType
function wrap<T>(value: T) {
return { value };
}
type Wrapped<T> = ReturnType<typeof wrap<T>>;
Section 7: Pitfalls and Tips
- Complexity vs readability: Advanced types are powerful but can obscure intent
-
Prefer clear intent before cleverness; add comments documenting the rationale for branding or
DeepPartialtypes - When in doubt, validate with a small runtime test to ensure the type-level trick aligns with runtime semantics
- Keep snippets minimal and focused; readers often skim dense type theory
Section 8: Real-World Integration Tips
- Introduce in-code comments near branded types to explain safety guarantees
- If you're adding a branded ID pattern, audit runtime code to ensure IDs don't slip through unbranded
- Use TypeScript's type-level utilities incrementally; start with
DeepPartialorDeepReadonlyfor configuration-heavy modules
Conclusion
TypeScript's power isn't only in what it can prevent at runtime, but in how it shapes safer, more expressive APIs with less boilerplate. The hidden gems above—short hands, aliases, and underutilized built-ins—are practical levers you can pull today to improve readability, safety, and developer experience in your codebase.
References and Credits
- TypeScript Handbook (type utilities, mapped types, conditional types)
- TypeScript 4.x features:
Awaited, template literal types,infer - Community patterns: branded types,
DeepPartial,DeepReadonlypatterns
What TypeScript hidden gems do you use in your projects? Share your favorites in the comments below!
Top comments (0)