TypeScriptBest PracticesCode Quality

TypeScript Patterns I Wish I Knew 5 Years Ago

By Ezequiel Carrizo
Ezquiel, the author Picture
Published on
TypeScript code on a monitor

I have been writing TypeScript for years, but I still keep discovering patterns that make me think "I wish I knew this five years ago." This post covers four of them that have genuinely changed how I write code daily.

Hi there! Want to support me?

1. Template Literal Types

Template literal types let you create string types that follow a pattern. Instead of accepting any string for a URL path, you can enforce the shape at the type level.

type Route = `/${string}`
type ApiEndpoint = `/api/${string}`
type BlogPost = `/blog/${string}`
const valid: Route = '/blog/typescript-patterns'
const alsoValid: ApiEndpoint = '/api/users'
// const invalid: Route = 'not-a-path' // Type error!

This is incredibly useful for route definitions, API paths, and any string that should follow a convention. Your editor autocompletes valid values, and invalid ones fail at compile time.

2. Type Guards That Actually Work

The is keyword in TypeScript is a type predicate. It tells the compiler that if a function returns true, the argument is of a specific type. This is the proper way to narrowunknown or union types without dangerous casts.

function isBlogPost(item: unknown): item is { slug: string; title: string } {
return (
typeof item === 'object' &&
item !== null &&
'slug' in item &&
'title' in item
)
}
const rawData: unknown = fetchData()
if (isBlogPost(rawData)) {
// TypeScript now knows rawData has slug and title
console.log(rawData.title.toUpperCase())
}

Type guards are essential when dealing with data from APIs, localStorage, or any untrusted source. Instead of blindly asserting types, you validate at runtime and get full type safety.

3. The satisfies Operator

Introduced in TypeScript 4.9, satisfies checks that a value conforms to a type without widening its inferred type. This means you get both type checking and the narrowest possible inference.

type BlogConfig = {
title: string
slug: string[]
tags: { title: string; slug: string[] }[]
}
const postConfig = {
title: 'My Blog Post',
slug: ['/blog', 'engineering', 'my-post/'],
tags: [
{ title: 'TypeScript', slug: ['/blog', 'engineering/'] }
]
} satisfies BlogConfig
// postConfig.slug is inferred as string[] with exact values, not widened to string[]

The key difference from a type annotation: with satisfies, postConfig.slug is inferred as ['/blog', 'engineering', 'my-post/'], not just string[]. You get autocomplete on the literal values while still being checked against the contract.

4. as const Assertions

as const tells TypeScript to infer the most specific literal type possible. Objects become deeply readonly, arrays become tuples, and strings become string literals.

const ROUTES = {
HOME: '/',
BLOG: '/blog/',
ABOUT: '/about/',
} as const
type Route = typeof ROUTES[keyof typeof ROUTES]
// Route = '/' | '/blog/' | '/about/'
function navigate(route: Route) {
// Only valid routes are accepted
}

I use as const for route maps, configuration objects, and any set of constants that should be treated as the source of truth. It enables exhaustiveness checks in switch statements and gives you type-safe lookups.

Hi there! Want to support me?

Putting It Together

What makes these patterns powerful is how they compose. A route definition becomes a const-asserted object of template literal types. An API response is validated with a type guard before being passed to a function that satisfies its contract. The compiler catches mistakes before they reach production.

Five years ago, I would have used any and hoped for the best. Today, I let the type system do the heavy lifting. If you are not using these patterns yet, pick one and start there — template literal types are the easiest win with the biggest impact.