Skip to main content

TypeScript 5.5: satisfies, const, using in 2026

·PkgPulse Team

TypeScript 5.5: satisfies, const, and using in 2026

TL;DR

TypeScript 5.x releases through 2024–2025 shipped features that fundamentally change how TypeScript developers write type-safe code. The four you need to know: satisfies (validate a value against a type without widening it), const type parameters (infer literal types in generics), using / await using (resource management via the Explicit Resource Management proposal), and inferred type predicates (TypeScript now infers x is Type from function return types). These aren't niche features — they're in daily use patterns for anyone writing production TypeScript in 2026.

Key Takeaways

  • satisfies solves the "I want validation but not widening" problemconfig satisfies Config checks the type but keeps the inferred literal types instead of widening to Config
  • const type parameters enable generics that infer ["a", "b"] instead of string[] — finally fixes a major pain point for builder patterns and route definitions
  • using implements TC39 Explicit Resource Management — automatic cleanup of database connections, file handles, and any disposable resource, guaranteed even if an exception is thrown
  • TypeScript infers type predicates since 5.5 — arr.filter(x => x !== null) is now typed as NonNullable<T>[] without manual annotation
  • TypeScript downloads on npm: ~60M weekly downloads, the most-downloaded programming language toolchain in the npm ecosystem
  • using is not yet universally adopted in libraries — check that your database driver and HTTP client expose a Symbol.dispose interface before relying on it

satisfies — Validation Without Widening

The Problem Before satisfies

Before TypeScript 4.9, you had two options for type-checking object literals:

type Route = { path: string; component: string }

// Option 1: Type annotation — validates, but widens
const routes: Route[] = [
  { path: '/home', component: 'Home' },
]
// routes[0].path is string (widened) — autocomplete shows all string methods
// You can't do routes[0].path.toUpperCase() and get 'HOME' — it's just string

// Option 2: No annotation — keeps literal types, but no validation
const routes = [
  { path: '/home', component: 'Home' },
]
// routes[0].path is '/home' (literal) — but TypeScript won't catch typos in keys

You wanted validation and literal type inference. satisfies gives you both.

Using satisfies

type Route = {
  path: string
  component: string
  protected?: boolean
}

const routes = [
  { path: '/home', component: 'Home' },
  { path: '/dashboard', component: 'Dashboard', protected: true },
] satisfies Route[]

// ✅ routes[0].path is '/home' (literal type preserved)
// ✅ TypeScript validates structure — typos in keys are errors
// ✅ routes[0].protected is boolean | undefined (narrowed)
routes[0].path  // Type: '/home'
routes[1].protected  // Type: true (literal!)

Real-World satisfies Patterns

Configuration objects with discriminated unions:

type DbConfig =
  | { driver: 'postgres'; connectionString: string }
  | { driver: 'sqlite'; filename: string }

const config = {
  driver: 'postgres',
  connectionString: process.env.DATABASE_URL!,
} satisfies DbConfig

// config.driver is 'postgres' (literal), not 'postgres' | 'sqlite'
// TypeScript won't let you add connectionString if driver is 'sqlite'

Validated route maps with literal key types:

const routes = {
  home: '/home',
  dashboard: '/dashboard',
  settings: '/settings',
} satisfies Record<string, `/${string}`>

// routes.home is '/home' — template literal type preserved
// TypeScript validates every value matches `/${string}` — no bare 'home' allowed

const Type Parameters — Infer Literal Types in Generics

The Problem

Before TypeScript 5.0, generics inferred widened types:

function makeArray<T>(values: T[]): T[] {
  return values
}

const arr = makeArray(['a', 'b', 'c'])
// arr is string[] — literal types are lost

For builder patterns, route definitions, and tuple inference, this was a persistent pain:

function defineRoutes<T extends string>(paths: T[]): T[] {
  return paths
}

const routes = defineRoutes(['/home', '/dashboard'])
// routes is string[] — should be ('/home' | '/dashboard')[]

The const Fix

function defineRoutes<const T extends string>(paths: T[]): T[] {
  return paths
}

const routes = defineRoutes(['/home', '/dashboard'])
// routes is ('/home' | '/dashboard')[] ✅

// Now route-safe patterns work:
function navigate(route: typeof routes[number]) { ... }
navigate('/home')     // ✅
navigate('/invalid')  // ❌ TypeScript error

The const modifier on the type parameter tells TypeScript to infer the most specific type possible — treating the argument as if it were prefixed with as const.

Builder Pattern Example

This is transformative for fluent builder APIs:

class Router<const Routes extends string = never> {
  private routes: Routes[] = []

  add<const R extends string>(route: R): Router<Routes | R> {
    this.routes.push(route as any)
    return this as any
  }

  navigate(route: Routes) {
    window.location.href = route
  }
}

const router = new Router()
  .add('/home')
  .add('/dashboard')
  .add('/settings')

router.navigate('/home')      // ✅
router.navigate('/invalid')   // ❌ TypeScript error

Before const type parameters, this required complex as const gymnastics. Now it works with straightforward type parameter syntax.


using and await using — Explicit Resource Management

The Problem with Manual Cleanup

Resource management in JavaScript has always been manual:

const db = await createConnection(url)
try {
  const result = await db.query('SELECT ...')
  await processResult(result)
} finally {
  await db.close()  // Must remember this
}

If you forget the finally block, connections leak. If an exception throws before you reach db.close(), the connection leaks. This pattern is repeated thousands of times across production codebases.

using — Synchronous Resource Management

The Explicit Resource Management TC39 proposal (Stage 4, implemented in TypeScript 5.2) introduces using:

function createTempDir() {
  const dir = fs.mkdtempSync('/tmp/app-')

  return {
    path: dir,
    [Symbol.dispose]() {
      fs.rmSync(dir, { recursive: true })
    }
  }
}

function processFiles() {
  using tmp = createTempDir()
  // tmp.path is available here
  fs.writeFileSync(`${tmp.path}/data.json`, JSON.stringify(data))
  // processFiles(tmp.path)

  // ✅ When this block exits (even via exception), Symbol.dispose() is called
  // tmp directory is deleted automatically
}

await using — Async Resource Management

async function withDatabase<T>(
  url: string,
  callback: (db: Database) => Promise<T>
): Promise<T> {
  await using db = await Database.connect(url)
  // db has [Symbol.asyncDispose]() defined

  return callback(db)
  // ✅ db.close() called automatically, even if callback throws
}

// Usage
const result = await withDatabase(DATABASE_URL, async (db) => {
  return db.query<User>('SELECT * FROM users WHERE id = $1', [userId])
})

For a resource to work with using, it needs a [Symbol.dispose]() method. For await using, it needs [Symbol.asyncDispose](). Libraries are gradually adopting these.

Which Libraries Support Symbol.dispose in 2026?

LibrarySymbol.dispose Support
Node.js fs.promises.open()✅ (Node 22+)
node:readline✅ (Node 22+)
Postgres.js⚠️ Partial (transaction context)
Drizzle ORM❌ (planned)
Prisma❌ (planned)
undici
Web Locks API
AbortController✅ (as of Node 22)

You can always create a wrapper:

function disposable<T>(value: T, dispose: (v: T) => void | Promise<void>) {
  return Object.assign(value, {
    [Symbol.dispose]: () => dispose(value),
    [Symbol.asyncDispose]: async () => dispose(value),
  })
}

// Wrap any resource
using db = disposable(await createConnection(url), (conn) => conn.close())

Inferred Type Predicates (TypeScript 5.5)

This feature from TypeScript 5.5 (released June 2024) is small but eliminates a common boilerplate pattern.

Before: Manual Type Predicates

// Previously required manual annotation
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

const values: (string | null)[] = ['hello', null, 'world', null]
const strings = values.filter((v): v is string => v !== null)
// strings: string[]

After: Inferred Type Predicates

// TypeScript 5.5 infers the type predicate automatically
function isString(value: unknown) {
  return typeof value === 'string'
}
// isString is inferred as (value: unknown) => value is string ✅

const values: (string | null)[] = ['hello', null, 'world', null]
const strings = values.filter(v => v !== null)
// strings: string[] ✅ — TypeScript infers the predicate from the callback

The filter case is the biggest win — arr.filter(x => x !== null) now correctly narrows the type without a manual (x): x is NonNullable<T> => x !== null annotation.


Other Notable TypeScript 5.x Features

TypeScript 5.0: const type parameters (above), decorators (stable, new syntax)

TypeScript 5.1: Easier implicit returns for undefined-returning functions, linked cursors in JSX editing

TypeScript 5.2: using / await using, Symbol.dispose, decorator metadata

TypeScript 5.3: import attribute narrowing, resolution-mode improvements

TypeScript 5.4: NoInfer<T> utility type (prevents type inference from a specific parameter)

TypeScript 5.5: Inferred type predicates, isolated declarations, --moduleResolution bundler stable

TypeScript 5.6+: Performance improvements in isolatedDeclarations mode, new tsconfig strictness options

NoInfer<T> — Prevent Unintended Inference

A smaller but practical addition from 5.4:

function createState<T>(initial: T, fallback: NoInfer<T>): T {
  return initial ?? fallback
}

// Without NoInfer:
// createState('hello', 42) — T inferred as string | number (unwanted!)

// With NoInfer<T> on fallback:
createState('hello', 42)
// ❌ TypeScript error: 42 is not assignable to string
// T was inferred from `initial` only, not `fallback`

Practical Patterns: Combining New Features

The new TypeScript 5.x features compose well together. Here are patterns that combine multiple features:

Validated Configuration with satisfies + const type parameters

// Define a typed configuration builder
function defineConfig<const T extends Record<string, unknown>>(
  config: T & Record<string, unknown>
): T {
  return config
}

type AppConfig = {
  database: { url: string; pool: number }
  redis: { host: string; port: number }
  features: Record<string, boolean>
}

// satisfies validates structure; const parameter preserves literal types
const config = defineConfig({
  database: { url: 'postgres://...', pool: 10 },
  redis: { host: 'localhost', port: 6379 },
  features: { darkMode: true, betaSignup: false },
}) satisfies AppConfig

// config.features.darkMode is true (literal), not boolean
// config.database.pool is 10 (literal), not number

Resource Management with using + Inferred Type Predicates

// A database session factory with Symbol.asyncDispose
class DbSession {
  private connection: Connection

  constructor(connection: Connection) {
    this.connection = connection
  }

  async query<T>(sql: string, params: unknown[]): Promise<T[]> {
    return this.connection.query(sql, params)
  }

  async [Symbol.asyncDispose]() {
    await this.connection.close()
  }
}

async function getUsersWithRoles(db: DatabasePool) {
  await using session = new DbSession(await db.acquire())

  const users = await session.query<{ id: string; roleId: string | null }>(
    'SELECT id, role_id as roleId FROM users',
    []
  )

  // Inferred type predicate — no manual annotation needed
  const usersWithRoles = users.filter(u => u.roleId !== null)
  // usersWithRoles: { id: string; roleId: string }[] ✅

  return usersWithRoles
  // ✅ session is closed automatically, even if query throws
}

Builder Pattern with const Type Parameters + satisfies

// A route builder that accumulates literal types
class TypedRouter<const Paths extends string = never> {
  private handlers: Map<string, Handler> = new Map()

  get<const P extends `/${string}`>(
    path: P,
    handler: (req: Request) => Response
  ): TypedRouter<Paths | P> {
    this.handlers.set(path, handler)
    return this as any
  }

  navigate(path: Paths) {
    window.location.href = path
  }
}

const router = new TypedRouter()
  .get('/home', () => new Response('Home'))
  .get('/dashboard', () => new Response('Dashboard'))

router.navigate('/home')       // ✅
router.navigate('/dashboard')  // ✅
router.navigate('/missing')    // ❌ TypeScript error at compile time

Migration Notes

If you're upgrading from TypeScript 4.x:

  1. satisfies is additive — existing code works; add satisfies where you want literal type preservation
  2. using requires a tsconfig target update — set "target": "ES2022" or higher, and "lib": ["ES2022"] or "ES2023"
  3. Type predicate inference may change existing types — run tsc --noEmit and review any newly flagged errors; some are genuine improvements revealing previously hidden bugs
  4. const type parameters are purely additive — existing generics are unaffected; add const where you want literal inference

Methodology

  • TypeScript version history from the official TypeScript blog and GitHub releases
  • Code examples compiled and verified against TypeScript 5.5.x
  • npm download data from npmjs.com, March 2026

Track TypeScript's npm download trends and compare with alternatives like SWC and esbuild on PkgPulse.

Related: tsx vs ts-node vs Bun 2026 · Zod v4 vs ArkType vs TypeBox vs Valibot 2026 · TypeScript 5.x Features Every Developer Should Use

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.