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

Published: (December 5, 2025 at 01:43 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

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, and 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.

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 = {
  [P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
};

// Usage
type User = {
  id: string;
  profile: {
    name: string;
    bio?: string;
  };
  roles: string[];
};

type UpdateUser = DeepPartial;

// Accepts e.g.:
// { profile: { name?: string }, roles?: string[] }

NonNullable in Practice

Extract non‑nullable values to ensure type safety:

type SafeProp = T[K] extends null | undefined ? never : T[K];

// Example constraint
function getName(obj: T): string {
  return obj.name ?? "anonymous";
}

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

  • Interfaces: public API shapes and object literals you intend to extend.
  • Aliases: unions, primitives, or branded types.

Mapped Types and Conditional Tricks

Mutable Pattern

Remove readonly modifiers when you need mutability:

type Mutable = { -readonly [P in keyof T]: T[P] };

type ReadonlyPoint = Readonly;
type Point = Mutable;

const p: Point = { x: 1, y: 2 };
p.x = 3; // ✅ allowed

DeepReadonly (Read‑Only Everywhere)

Lock down entire configuration objects:

type DeepReadonly = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P];
};

type Config = {
  server: { host: string; port: number };
  features: string[];
};

type SafeConfig = DeepReadonly;

Distributive Conditional Tricks

type IsStringLike = T extends string ? true : false;
type A = IsStringLike; // true | false

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 extends (infer U)[] ? U : T;

type T1 = ElementType; // string
type T2 = ElementType;   // number

Awaited for Unwrapping Promises

async function fetchData(): Promise {
  return { ok: true };
}
type Data = Awaited>; // { ok: boolean }

Template Literal Types for Chemistry Between Strings and Types

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

function navigate(path: RoutePath) {
  // runtime navigation
}

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 them where they solve a real pain point.

Practical Patterns with Concrete, Minimal Examples

Example 1: Safe Configuration Loader

type Config = {
  port?: number;
  host?: string;
};

type RequiredConfig = Required;

function loadConfig(input: Partial): 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(value: T) {
  return { value };
}

type Wrapped = ReturnType>;

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.

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 are consistently created and consumed.
Back to Blog

Related posts

Read more »