DEV Community

Cover image for Hidden Gems in TypeScript - Short Hands, Aliases, and Underutilized Built-ins That Save You Time
Akash
Akash

Posted on

Hidden Gems in TypeScript - Short Hands, Aliases, and Underutilized Built-ins That Save You Time

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 const to 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, infer in 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[] }
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

Distributive Conditional Tricks

type IsStringLike<T> = T extends string ? true : false;
type A = IsStringLike<string | number>; // true | false
Enter fullscreen mode Exit fullscreen mode

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[]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Awaited for Unwrapping Promises

async function fetchData(): Promise<{ ok: boolean }> {
  return { ok: true };
}
type Data = Awaited<ReturnType<typeof fetchData>>; // { ok: boolean }
Enter fullscreen mode Exit fullscreen mode

Template Literal Types for Chemistry Between Strings and Types

type RoutePath = `/users/${string}` | `/projects/${string}`;

function navigate(path: RoutePath) {
  // runtime navigation
}
Enter fullscreen mode Exit fullscreen mode

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 Awaited in 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" });
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Example 3: Function Ergonomics with Generics and ReturnType

function wrap<T>(value: T) {
  return { value };
}

type Wrapped<T> = ReturnType<typeof wrap<T>>;
Enter fullscreen mode Exit fullscreen mode

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 DeepPartial types
  • 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 DeepPartial or DeepReadonly for 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, DeepReadonly patterns

What TypeScript hidden gems do you use in your projects? Share your favorites in the comments below!

Top comments (0)