TypeScript 5 Mastery Guide 2026 — Advanced Types, Decorators, and Best Practices

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

TypeScript 5 closes the gap between type safety and developer ergonomics. Features like const type parameters, the satisfies operator, and variadic tuple types let you express complex constraints that were previously impossible without sacrificing usability.

const Type Parameters

Infer literal types without as const at every call site:

// Before TypeScript 5:
function createRoute<T extends string>(path: T) {
  return { path, handler: () => {} }
}
const route = createRoute('/users')
// route.path is string, not '/users'
 
// TypeScript 5: const type parameter
function createRoute<const T extends string>(path: T) {
  return { path, handler: () => {} }
}
const route = createRoute('/users')
// route.path is '/users' — literal type preserved!
 
// Especially powerful with objects:
function makeConfig<const T extends Record<string, unknown>>(config: T): T {
  return config
}
 
const config = makeConfig({
  port: 3000,
  env: 'production',
  features: ['auth', 'payments'],
})
// config.env is 'production', config.features is readonly ['auth', 'payments']

The satisfies Operator

Validate a value matches a type while keeping the inferred literal type:

type Route = {
  path: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  handler: () => Response
}
 
// Without satisfies: either loses literal types OR loses validation
const routes = {
  getUser: {
    path: '/users/:id',
    method: 'GET',
    handler: () => new Response(),
  },
} satisfies Record<string, Route>
 
// routes.getUser.method is 'GET' (not string)
// TypeScript still validates the structure
 
// Real-world: config objects
const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
} satisfies Record<string, string | number[]>
 
palette.red.map(v => v * 2)    // OK — inferred as number[]
palette.green.toUpperCase()     // OK — inferred as string

Template Literal Types

Build complex string types programmatically:

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiVersion = 'v1' | 'v2'
type Endpoint = 'users' | 'posts' | 'comments'
 
type ApiRoute = `/${ApiVersion}/${Endpoint}`
// '/v1/users' | '/v1/posts' | ... | '/v2/comments'
 
// Extracting parts of a string type
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
    ? Param
    : never
 
type Params = ExtractParams<'/users/:id/posts/:postId'>
// 'id' | 'postId'
 
// Event name generation
type EventMap<T extends string> = {
  [K in T as `on${Capitalize<K>}`]: (event: Event) => void
}
 
type ButtonEvents = EventMap<'click' | 'hover' | 'focus'>
// { onClick: ..., onHover: ..., onFocus: ... }

Decorators (Stage 3)

TypeScript 5 supports the TC39 Stage 3 decorators spec:

// Method decorator for logging
function log(target: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name)
 
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with`, args)
    const result = target.call(this, ...args)
    console.log(`${methodName} returned`, result)
    return result
  }
}
 
// Property decorator for validation
function required(target: undefined, context: ClassFieldDecoratorContext) {
  return function (this: any, value: any) {
    if (value === undefined || value === null) {
      throw new Error(`${String(context.name)} is required`)
    }
    return value
  }
}
 
class UserService {
  @required
  private apiKey: string
 
  constructor(apiKey: string) {
    this.apiKey = apiKey
  }
 
  @log
  async getUser(id: string) {
    const res = await fetch(`/api/users/${id}`, {
      headers: { Authorization: `Bearer ${this.apiKey}` },
    })
    return res.json()
  }
}

Variadic Tuple Types and Inference

// Concat two tuples with preserved types
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
 
type AB = Concat<[string, number], [boolean, Date]>
// [string, number, boolean, Date]
 
// Strongly typed pipe function
function pipe<A, B, C>(
  fn1: (a: A) => B,
  fn2: (b: B) => C
): (a: A) => C {
  return (a) => fn2(fn1(a))
}
 
const parseAndDouble = pipe(
  (s: string) => parseInt(s, 10),
  (n: number) => n * 2
)
 
const result = parseAndDouble('21')  // result: number — inferred correctly

Common Mistakes

  • Using any instead of unknown for values from external APIs — unknown forces type checking
  • Writing as Type casts instead of using satisfies or proper type guards
  • Defining overly wide types like object or Record<string, any> when a specific shape is known
  • Not using const assertions or const type parameters for configuration objects
  • Ignoring strictNullChecks — enabling it reveals real bugs in most codebases

Best Practices

  • Enable strict: true in tsconfig.json from day one
  • Use satisfies when you want validation without losing inferred literal types
  • Prefer unknown over any for external data, then narrow with type guards or Zod
  • Use const type parameters for functions that receive configuration objects
  • Write utility types with template literals to derive event maps, route params, and API types

Key Takeaways

  • const type parameters preserve literal types in generic functions without requiring as const at call sites
  • The satisfies operator validates structure while keeping the narrower inferred type
  • Template literal types can derive complex union types from string patterns
  • TypeScript 5 implements TC39 Stage 3 decorators — different API from the legacy experimentalDecorators
  • Variadic tuple types ([...T, ...U]) enable type-safe pipe and compose utilities
  • unknown is always preferable to any for external data — it forces explicit narrowing
  • strictNullChecks and strict: true catch the most bugs and should be enabled from project start
  • Use Zod or Valibot with TypeScript for runtime validation that stays in sync with your static types

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading