Agent skill

typescript-advanced

Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or enforcing type safety beyond basic annotations — discriminated unions with exhaustive checks, branded/opaque types for domain safety, satisfies vs as const decisions, NoInfer for inference control, module augmentation for third-party types, or choosing between hand-rolled types and type-fest utilities. Do not use for basic TypeScript syntax or simple type annotations.

Stars 3
Forks 0

Install this agent skill to your Project

npx add-skill https://github.com/trancong12102/agentskills/tree/main/typescript-advanced

SKILL.md

TypeScript Advanced: Patterns, Pitfalls & type-fest

This skill defines the rules, conventions, and architectural decisions for writing advanced TypeScript. It is intentionally opinionated to prevent common type-level bugs and enforce patterns that produce safe, maintainable code.

For detailed API documentation of TypeScript features, use other appropriate tools (documentation lookup, web search, etc.) — this skill focuses on when, why, and how to use advanced type features correctly.

Table of Contents

  1. Type Safety Philosophy
  2. Discriminated Unions & Exhaustive Checks
  3. Branded Types — Nominal Safety in a Structural System
  4. Modern Inference Tools
  5. type-fest: Don't Reinvent the Wheel
  6. Common Pitfalls
  7. Reference Files

Type Safety Philosophy

any vs unknown vs never — the only rule you need

Type Assignable from Assignable to Operations Use for
any anything anything all (UNSAFE) Never in new code
unknown anything only unknown / any none unnarowed External inputs, JSON, user data
never nothing anything none Exhaustive checks, unreachable code

Rule: never use any in new code. Use unknown for external boundaries and narrow before operating. Use never for exhaustiveness and impossible states.

Prefer unions over enums

typescript
// Avoid — numeric enums are structurally assignable to number (footgun)
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
function go(d: Direction) {}
go(42); // no error — TypeScript allows any number!

// Prefer — exhaustive, tree-shakeable, no runtime artifact
type Direction = "up" | "down" | "left" | "right";

String enums are safer than numeric but still carry runtime overhead and import friction. String literal unions are the default choice unless you need reverse mapping.

interface vs type — decision table

Scenario Use Why
Object shapes, class contracts interface Declaration merging, better error messages
Unions, intersections, mapped/conditional type Only type supports these
Third-party augmentation needed interface Only interfaces support declaration merging
Public API types (libraries) interface Consumers can augment; better display in tooltips
Internal computed types type More expressive, no accidental merging

Discriminated Unions & Exhaustive Checks

The never exhaustiveness pattern

Every switch / if-else chain on a discriminated union must handle all variants. Use the never assignment to get a compile-time error when a new variant is added:

typescript
type Result<T> =
  | { status: "ok"; data: T }
  | { status: "error"; error: Error }
  | { status: "loading" };

function handle<T>(result: Result<T>): string {
  switch (result.status) {
    case "ok":
      return JSON.stringify(result.data);
    case "error":
      return result.error.message;
    case "loading":
      return "Loading...";
    default:
      const _exhaustive: never = result;
      return _exhaustive; // compile error if a variant is unhandled
  }
}

Rules for discriminated unions

  • Discriminant must be a literal typestring, number, boolean literals. Wide types like string do not narrow.
  • Keep the discriminant property name consistent across all members (kind, type, status).
  • Avoid optional discriminantsstatus?: "ok" | "error" breaks narrowing.

Branded Types — Nominal Safety in a Structural System

TypeScript is structural: UserId (a string) and OrderId (a string) are interchangeable by default. Branded types break this at the type level with zero runtime overhead.

Recommended pattern: unique symbol brand

typescript
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

// Constructor = the single trust boundary, validate here
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: string): OrderId => id as OrderId;

function getUser(id: UserId) {
  /* ... */
}
getUser(toUserId("abc")); // ok
getUser(toOrderId("abc")); // ERROR — OrderId not assignable to UserId
getUser("abc"); // ERROR — string not assignable to UserId

When to use branded types

  • IDsUserId, OrderId, ProductId prevent cross-assignment
  • UnitsMeters, Feet, USD, EUR prevent arithmetic mistakes
  • Validated stringsEmail, URL, Slug encode that validation has happened
  • Opaque tokensJWTToken, APIKey prevent accidental logging/display

type-fest alternative

Use Tagged<T, Tag> from type-fest for multi-tag composition and metadata:

typescript
import type { Tagged, GetTagMetadata } from "type-fest";
type UserId = Tagged<string, "UserId">;
type AdminId = Tagged<UserId, "Admin">; // composable — both tags preserved

Modern Inference Tools

satisfies — validate without widening (TS 4.9+)

typescript
type Theme = Record<"primary" | "secondary", string | string[]>;

// Type annotation: loses specific types
const t1: Theme = { primary: "#000", secondary: ["#111", "#222"] };
t1.secondary.map((s) => s); // ERROR — string | string[] has no .map

// satisfies: validates structure, keeps specific inference
const t2 = { primary: "#000", secondary: ["#111", "#222"] } satisfies Theme;
t2.secondary.map((s) => s); // ok — inferred as string[]

Use satisfies when: you want config validation (catch typos in keys) but also need autocomplete on specific values.

const type parameters — generic literal inference (TS 5.0+)

typescript
// Without const: T = string[]
function routes<T extends string[]>(r: T): T {
  return r;
}

// With const: T = readonly ["users", "posts"]
function routes<const T extends string[]>(r: T): T {
  return r;
}
const r = routes(["users", "posts"]); // readonly ["users", "posts"]

Use const type parameters when: building registries, config factories, or any generic where preserving literal types at the call site matters.

NoInfer<T> — control inference sources (TS 5.4+)

Prevents a parameter from contributing to type inference — it reads T but doesn't influence what T becomes:

typescript
function createFSM<const TState extends string>(config: {
  states: TState[];
  initial: NoInfer<TState>; // must be from states, can't introduce new values
}) {
  /* ... */
}

createFSM({ states: ["idle", "running"], initial: "idle" }); // ok
createFSM({ states: ["idle", "running"], initial: "stopped" }); // ERROR

Use NoInfer when: a function has multiple parameters sharing a type param, and one should be constrained to what the others infer — not contribute new candidates.


type-fest: Don't Reinvent the Wheel

type-fest provides 200+ utility types with zero runtime cost (types-only). Always check type-fest before writing a custom utility.

typescript
import type { Simplify, Merge, SetRequired, LiteralUnion } from "type-fest";

Decision table: built-in vs type-fest

Need Built-in type-fest
Make keys optional/required Partial, Required SetOptional, SetRequired (per-key)
Deep partial/readonly PartialDeep, ReadonlyDeep
Merge two types (override, not intersect) Merge, MergeDeep
Flatten intersection for readability Simplify
String union with autocomplete + string LiteralUnion
At-least/exactly-one constraint RequireAtLeastOne, RequireExactlyOne
Nominal/branded types Tagged, UnwrapTagged
JSON round-trip type Jsonify
Strict omit (key must exist) Omit (loose) Except (strict)
Deep dot-path access Paths, Get
Exact object (reject excess props) Exact
Pick/omit by value type ConditionalPick, ConditionalExcept
Package.json / tsconfig types PackageJson, TsConfigJson
Case conversion for keys CamelCasedProperties, etc.

Most commonly needed utilities

  • Simplify<T> — flattens A & B & C into readable { ...all keys }. Use on any intersection that produces unreadable hover tooltips.
  • Merge<A, B>A & B produces never when keys conflict; Merge cleanly overrides. Use instead of & when types share key names.
  • LiteralUnion<Literal, Base>'a' | 'b' | string kills autocomplete; LiteralUnion preserves it. Essential for extensible string APIs.
  • SetRequired<T, K> / SetOptional<T, K> — toggle specific keys without maintaining duplicate interfaces.
  • Jsonify<T> — models JSON.parse(JSON.stringify(x)). Catches Datestring, undefined → dropped, interface open-index issues.

Common Pitfalls

  1. any leaks silently — one any propagates through assignments, generics, and return types. A single any in a utility type makes all downstream types unsound. Use unknown + narrowing instead.

  2. Excess property checks only apply to literals — assigning through a variable bypasses excess property checks entirely. Don't rely on them for runtime safety.

    typescript
    interface Point {
      x: number;
      y: number;
    }
    const obj = { x: 1, y: 2, z: 3 };
    const p: Point = obj; // no error — z slips through
    
  3. Distributive conditional type on neverT extends X ? A : B where T is never returns never (not B). Wrap in tuples: [T] extends [X].

  4. Omit doesn't check key existenceOmit<T, "typo"> silently succeeds. Use Except from type-fest for strict key checking.

  5. Type widening with letlet x = "hello" is string, not "hello". Use const, as const, or satisfies to preserve literals.

  6. & intersection with conflicting keys{ a: string } & { a: number } makes a: never. Use Merge from type-fest instead.

  7. Enum numeric assignabilityenum Foo { A, B } allows const x: Foo = 999. Use string literal unions instead.

  8. interface accidental merging — two interface User {} declarations in the same scope silently merge. Use type for internal types that should not be extended.

  9. const enum under isolatedModules — esbuild, SWC, Babel all use isolatedModules. const enum in .d.ts or library code breaks these builds.

  10. Forgetting readonly on array parametersfunction f(arr: string[]) allows mutation. Use readonly string[] for params you don't intend to mutate.

  11. Structural subtyping function params — method syntax push(x: T) is bivariant (unsound). Use function property syntax push: (x: T) => void under strictFunctionTypes for correct variance.

  12. Reinventing type-fest utilities — check type-fest before writing DeepPartial, DeepReadonly, Merge, branded types, or key manipulation types. The library handles edge cases (circular refs, readonly arrays, maps/sets) that hand-rolled versions miss.


Reference Files

Read the relevant reference file when working with a specific pattern:

File When to read
references/conditional-types.md infer, distributive conditionals, constraining with extends
references/mapped-types.md Key remapping, filtering, template literal key manipulation
references/template-literals.md String manipulation at type level, pattern matching, parsing
references/module-augmentation.md Declaration merging, extending third-party types, global scope
references/type-fest.md Full type-fest utility catalog by category with usage examples

Expand your agent's capabilities with these related and highly-rated skills.

trancong12102/agentskills

deps-dev

Look up the latest stable version of any open-source package across npm, PyPI, Go, Cargo, Maven, and NuGet. Use when the user asks 'what's the latest version of X', 'what version should I use', 'is X deprecated', 'how outdated is my package.json/requirements.txt/Cargo.toml', or needs version numbers for adding or updating dependencies. Also covers pinning versions, checking if packages are maintained, or comparing installed vs latest versions. Do NOT use for private/internal packages or for looking up documentation (use context7).

3 0
Explore
trancong12102/agentskills

github-codebase-search

Semantic search for public GitHub repos without cloning. Use when the user wants to understand how an external library or framework works internally, investigate upstream bugs, trace code paths in a repo they haven't cloned, or search GitHub source code by intent. Do NOT use for local codebase questions (use codebase-search), documentation lookup (use context7), or private repos.

3 0
Explore
trancong12102/agentskills

council-review

Multi-model AI code review — runs Codex, Claude, and Simplify reviews in parallel, then synthesizes a unified report. Use when the user asks to review code changes, audit a diff, check code quality, review a PR, review commits, or review uncommitted changes. Also covers 'code review', 'review my changes', 'check this before I merge', or wanting multiple perspectives on code. Do NOT use for documentation/markdown review or trivial single-line changes.

3 0
Explore
trancong12102/agentskills

react-native-advanced

React Native and Expo patterns for navigation, data fetching lifecycle, infinite scroll lists, form handling, state persistence, authentication routing, gesture-driven animations, bottom sheets, push notifications, and OTA updates. Use when building Expo/React Native apps that need screen-level data prefetching, auth guards with protected routes, infinite scroll feeds, native form input handling, offline-capable state persistence, platform-specific setup (focus/online managers), fluid animations and gesture interactions, modal bottom sheets, push notification flows, or over-the-air update strategies. Do not use for React web apps.

3 0
Explore
trancong12102/agentskills

react-web-advanced

Web-specific React patterns for type-safe file-based routing, route-level data loading, server-side rendering, search param validation, code splitting, and list virtualization. Use when building React web apps with route loaders, SSR streaming, validated search params, lazy route splitting, or virtualizing large DOM lists. Do not use for React Native apps — use react-native-advanced instead.

3 0
Explore
trancong12102/agentskills

context7

Fetch up-to-date documentation for any open-source library or framework. Use when the user asks to look up docs, check an API, find code examples, or verify how a feature works — especially with a specific library name, version migration, or phrases like 'what's the current way to...' or 'the API might have changed'. Also covers setup and configuration docs. Do NOT use for general programming concepts, internal project code, or version lookups (use deps-dev).

3 0
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results