惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

I
InfoQ
Spread Privacy
Spread Privacy
GbyAI
GbyAI
F
Fortinet All Blogs
小众软件
小众软件
B
Blog RSS Feed
博客园_首页
量子位
Y
Y Combinator Blog
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
T
The Blog of Author Tim Ferriss
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Vercel News
Vercel News
Last Week in AI
Last Week in AI
F
Full Disclosure
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
宝玉的分享
宝玉的分享
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
A
About on SuperTechFans
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Tenable Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
Engineering at Meta
Engineering at Meta
博客园 - 叶小钗
S
SegmentFault 最新的问题
T
Threat Research - Cisco Blogs
博客园 - 司徒正美
MyScale Blog
MyScale Blog
云风的 BLOG
云风的 BLOG
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
The GitHub Blog
The GitHub Blog
V
Vulnerabilities – Threatpost
S
Schneier on Security
Latest news
Latest news
I
Intezer
A
Arctic Wolf
T
Threatpost

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
TypeScript Tips That Actually Matter in Real Projects (including the satisfies operator)
Gavin Cettolo · 2026-06-24 · via DEV Community

Most TypeScript tutorials teach you the language.

This article teaches you how to use it.

There's a difference. The language has hundreds of features. A real project uses maybe twenty of them regularly, and about eight of them make up the difference between TypeScript that fights you and TypeScript that helps you.

These are those eight.

Each one comes from a pattern I've seen repeatedly in real codebases: first as an antipattern, then as a realization, then as a habit. The goal isn't to show off advanced type gymnastics. It's to show you the specific things that make your code safer, more readable, and less painful to maintain.


TL;DR

  • Most TypeScript pain comes from fighting the type system instead of working with it, any, manual casting, and loose types are the usual culprits.
  • A small set of features, discriminated unions, utility types, satisfies, as const, generics, solve the majority of real-world typing problems.
  • The best TypeScript isn't the most complex. It's the most precise.

Table of Contents

  • Tip 1: Use Discriminated Unions Instead of Optional Fields
  • Tip 2: Stop Writing Types Twice with Utility Types
  • Tip 3: Use satisfies to Validate Without Losing Inference
  • Tip 4: Use as const for Literal Types That Don't Drift
  • Tip 5: Write Type Guards Instead of Casting
  • Tip 6: Use Generics to Write Functions Once
  • Tip 7: Use ReturnType and Parameters to Stay in Sync
  • Tip 8: Use unknown Instead of any for External Data
  • Honorable Mentions
  • Final Thoughts

Tip 1: Use Discriminated Unions Instead of Optional Fields

This is the tip that changes how you model data in TypeScript. Once you see it, you'll spot the antipattern everywhere.

The antipattern

// ❌ A type that tries to represent multiple states with optional fields
interface ApiResponse {
  data?: User
  error?: string
  isLoading: boolean
}

The problem: this type allows impossible states. Nothing stops you from having both data and error set at the same time, or neither set, or isLoading: false with no data and no error.

The type says "any combination of these fields is valid." Your domain says only three combinations are valid: loading, success, or error. The type isn't telling the truth.

The fix: discriminated unions

// ✅ Each state is explicit and mutually exclusive
type ApiResponse<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }

Now TypeScript knows exactly which fields exist in each state. When you narrow on status, you get precise autocomplete and type safety:

function renderUser(response: ApiResponse<User>) {
  switch (response.status) {
    case 'loading':
      return 'Loading...'
    case 'success':
      return response.data.name  // TypeScript knows data exists here
    case 'error':
      return response.error      // TypeScript knows error exists here
  }
}

If you add a new state to ApiResponse and forget to handle it in the switch, TypeScript tells you. That's the real payoff: exhaustiveness checking. The type system enforces that every case is handled.

This pattern is especially powerful for modelling form states, async operations, and anything with multiple mutually exclusive conditions.


Tip 2: Stop Writing Types Twice with Utility Types

TypeScript ships with a set of built-in utility types that most developers underuse. They exist specifically to prevent you from duplicating type definitions.

The antipattern

// ❌ Manually maintaining two overlapping types
interface User {
  id: string
  email: string
  firstName: string
  lastName: string
  role: 'admin' | 'user'
  createdAt: string
}

// Manually duplicated, will drift from User when User changes
interface UpdateUserPayload {
  email?: string
  firstName?: string
  lastName?: string
}

// Manually duplicated again
interface UserPreview {
  id: string
  firstName: string
  lastName: string
}

When User changes, you have to remember to update every manual derivative. You won't always remember.

The fix: utility types

// ✅ Derived from the source of truth, always in sync
interface User {
  id: string
  email: string
  firstName: string
  lastName: string
  role: 'admin' | 'user'
  createdAt: string
}

// Partial makes all fields optional
type UpdateUserPayload = Partial<Pick<User, 'email' | 'firstName' | 'lastName'>>

// Pick selects specific fields
type UserPreview = Pick<User, 'id' | 'firstName' | 'lastName'>

// Omit removes specific fields
type UserWithoutMeta = Omit<User, 'createdAt' | 'role'>

// Required makes all fields required (opposite of Partial)
type StrictUser = Required<User>

// Readonly makes all fields immutable
type ImmutableUser = Readonly<User>

The key insight: UpdateUserPayload is now derived from User. When you add a field to User, you decide whether to include it in the derived types, but the base type is the single source of truth.

A practical combination that appears constantly in REST APIs:

// Create payload: no id or metadata, those are server-generated
type CreateUserPayload = Omit<User, 'id' | 'createdAt'>

// Update payload: everything optional except what can't change
type UpdateUserPayload = Partial<Omit<User, 'id' | 'createdAt' | 'role'>>


Tip 3: Use satisfies to Validate Without Losing Inference

satisfies was introduced in TypeScript 4.9 and is still underused. It solves a specific problem that as and explicit type annotations both fail to handle cleanly.

The problem

When you annotate a variable with a type, TypeScript widens the inferred type to match the annotation, and you lose specific information:

// ❌ Explicit annotation loses specific type information
const config: Record<string, string> = {
  host: 'localhost',
  port: '5432',
}

config.host  // type: string, you've lost 'localhost'
config.nonExistent  // No error! The index signature allows any key

When you skip the annotation, you get precise inference but no validation against a shape:

// ❌ No annotation, no validation
const config = {
  host: 'localhost',
  port: '5432',
}

config.host         // type: 'localhost' ✅
config.nonExistent  // Error ✅, but you have no shape validation

The fix: satisfies

// ✅ Validates against the shape AND preserves specific types
type AppConfig = {
  host: string
  port: string
  database: string
}

const config = {
  host: 'localhost',
  port: '5432',
  database: 'myapp',
  unknownKey: 'oops',  // ❌ Error: Object literal may only specify known properties
} satisfies AppConfig

config.host  // type: 'localhost', specific type preserved ✅

satisfies tells TypeScript: "validate that this value matches this type, but infer the most specific type possible." You get the validation of an annotation and the precision of inference.

Especially useful for configuration objects, theme definitions, and route maps:

type Routes = Record<string, { path: string; exact?: boolean }>

const routes = {
  home: { path: '/' },
  about: { path: '/about' },
  profile: { path: '/profile/:id', exact: true },
} satisfies Routes

routes.home.path   // type: '/', not just string
routes.unknown     // ❌ Error, unknown key caught at compile time


Tip 4: Use as const for Literal Types That Don't Drift

as const is one of the most practical tools in TypeScript for working with fixed sets of values. It tells the compiler: "don't widen this, keep every value as its literal type."

The antipattern

// ❌ TypeScript widens these to string[], you lose the specific values
const STATUSES = ['pending', 'active', 'cancelled', 'completed']

type Status = typeof STATUSES[number]  // type: string, not what you wanted

The fix

// ✅ as const preserves literal types
const STATUSES = ['pending', 'active', 'cancelled', 'completed'] as const

type Status = typeof STATUSES[number]
// type: 'pending' | 'active' | 'cancelled' | 'completed' ✅

// The array itself is also typed precisely
const firstStatus = STATUSES[0]  // type: 'pending'

This pattern is particularly powerful for objects used as enums or configuration maps:

const HTTP_CODES = {
  OK: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  NOT_FOUND: 404,
  INTERNAL_ERROR: 500,
} as const

type HttpCode = typeof HTTP_CODES[keyof typeof HTTP_CODES]
// type: 200 | 201 | 400 | 401 | 404 | 500

function handleResponse(code: HttpCode) {
  // TypeScript knows exactly which values are valid
}

handleResponse(HTTP_CODES.OK)   // ✅
handleResponse(999)              // ❌ Error: 999 is not assignable to HttpCode

as const also pairs naturally with discriminated unions: define your discriminant values as constants, derive the union type, and you have a single source of truth for both runtime values and compile-time types.


Tip 5: Write Type Guards Instead of Casting

Type casting with as is the TypeScript equivalent of // eslint-disable, it silences the compiler without solving the problem. Every as is a place where TypeScript's safety guarantee ends.

The antipattern

// ❌ Casting tells TypeScript to trust you, and you might be wrong
async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`)
  const data = await response.json()
  return data as User  // TypeScript believes you. The runtime doesn't care.
}

If the API returns a different shape than User, TypeScript won't tell you. The bug surfaces at runtime, in production, when someone accesses user.firstName and gets undefined.

The fix: type guards

// ✅ Validate the shape at runtime, TypeScript trusts what you verify

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'email' in value &&
    'firstName' in value &&
    typeof (value as any).id === 'string' &&
    typeof (value as any).email === 'string'
  )
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`)
  const data: unknown = await response.json()

  if (!isUser(data)) {
    throw new Error(`Invalid user response for id: ${id}`)
  }

  return data  // TypeScript now knows this is User, and it's been verified
}

The value is User return type is a type predicate: it tells TypeScript that if the function returns true, the value can be treated as User in the narrowed scope.

For production codebases with complex external data, consider Zod for runtime validation, it generates TypeScript types from schemas and validates data at the boundary in one step. We covered the full pattern in API Calls Done Right.


Tip 6: Use Generics to Write Functions Once

Generics are the feature most developers avoid the longest, and regret avoiding. They're the tool that lets you write a function once and have it work correctly across multiple types, without sacrificing type safety.

The antipattern

// ❌ Three functions doing the same thing for different types
function getFirstUser(items: User[]): User | undefined {
  return items[0]
}

function getFirstProduct(items: Product[]): Product | undefined {
  return items[0]
}

function getFirstOrder(items: Order[]): Order | undefined {
  return items[0]
}

Or worse, using any to avoid repetition:

// ❌ any kills type safety entirely
function getFirst(items: any[]): any {
  return items[0]
}

The fix: generics

// ✅ One function, full type safety across all types
function getFirst<T>(items: T[]): T | undefined {
  return items[0]
}

const firstUser = getFirst(users)      // type: User | undefined
const firstProduct = getFirst(products) // type: Product | undefined
const firstNumber = getFirst([1, 2, 3]) // type: number | undefined

Generics become especially powerful with constraints, limiting which types are accepted:

// Only works with objects that have an id field
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id)
}

findById(users, 'user-1')      // ✅ User has id
findById(products, 'prod-1')   // ✅ Product has id
findById([1, 2, 3], 'x')       // ❌ Error: number doesn't have id

A real-world example, a typed API fetcher:

async function apiFetch<T>(url: string): Promise<T> {
  const response = await fetch(url)
  if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
  return response.json() as Promise<T>
}

// The return type is inferred from the type parameter
const user = await apiFetch<User>('/api/users/1')     // type: User
const products = await apiFetch<Product[]>('/api/products') // type: Product[]

The rule of thumb: when you find yourself writing the same function structure for multiple types, that's a generic waiting to be extracted.


Tip 7: Use ReturnType and Parameters to Stay in Sync

When you call a function from an external library, or from another part of your codebase, you often need types that match its return value or arguments. The naive approach is to manually write those types. The problem is that manual types drift.

The antipattern

// ❌ Manually written type that will drift from the actual function
interface UserServiceResult {
  id: string
  name: string
  // ...but what if userService.getUser() changes?
}

function processUser(user: UserServiceResult) {
  // ...
}

When userService.getUser() adds a new field, UserServiceResult doesn't update automatically. You have a type that lies about the actual data.

The fix: ReturnType and Parameters

// ✅ Types derived directly from the functions, always in sync

function getUser(id: string) {
  return {
    id,
    firstName: 'Gavin',
    lastName: 'Cettolo',
    role: 'admin' as const,
    createdAt: new Date().toISOString(),
  }
}

// Automatically matches whatever getUser returns
type User = ReturnType<typeof getUser>

// Automatically matches whatever getUser accepts
type GetUserParams = Parameters<typeof getUser>
// type: [id: string]

function processUser(user: User) {
  // user has exactly the shape of getUser's return value
  // If getUser changes, processUser is immediately aware
  console.log(user.firstName)
}

This is particularly useful when working with third-party libraries where you don't control the type definitions:

import { createStore } from 'some-library'

// Extract the return type without importing a separate type
type Store = ReturnType<typeof createStore>

// Extract what the function expects
type StoreConfig = Parameters<typeof createStore>[0]

Other intrinsic utility types worth knowing alongside these:

  • Awaited<T>, unwraps a Promise type: Awaited<Promise<User>>User
  • InstanceType<T>, extracts the instance type from a constructor

Tip 8: Use unknown Instead of any for External Data

any is TypeScript's escape hatch. It tells the compiler: "I know what I'm doing, stop checking." The problem is that you often don't know, especially with external data.

unknown is the safe alternative. It represents a value you haven't inspected yet. TypeScript forces you to narrow it before you can use it.

The antipattern

// ❌ any propagates silently, errors hide until runtime
function parseConfig(raw: any) {
  return {
    port: raw.port,           // No error even if raw.port doesn't exist
    host: raw.host.trim(),    // Runtime crash if host is undefined
    debug: raw.debug,
  }
}

With any, TypeScript assumes every property access and method call is valid. Errors surface at runtime, not at compile time.

The fix: unknown with narrowing

// ✅ unknown forces you to verify before you use
function parseConfig(raw: unknown): AppConfig {
  if (
    typeof raw !== 'object' ||
    raw === null ||
    !('port' in raw) ||
    !('host' in raw)
  ) {
    throw new Error('Invalid config shape')
  }

  const { port, host } = raw as { port: unknown; host: unknown }

  if (typeof port !== 'number' || typeof host !== 'string') {
    throw new Error('Invalid config field types')
  }

  return { port, host }
}

The places where unknown matters most:

// API responses
const data: unknown = await response.json()

// Error handling, errors thrown in try/catch are unknown
try {
  await riskyOperation()
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message)  // Safe: TypeScript knows it's an Error
  }
}

// JSON.parse, the return type is any by default, worth narrowing immediately
const parsed: unknown = JSON.parse(rawString)

The rule: use any only when you're in a migration path from untyped code and you need to move fast. For all external data boundaries, APIs, JSON parsing, error handling, use unknown and narrow explicitly.


Honorable Mentions

Three more patterns that didn't make the full treatment but are worth knowing.

Template Literal Types

// Generate precise string types programmatically
type EventName = 'click' | 'focus' | 'blur'
type HandlerName = `on${Capitalize<EventName>}`
// type: 'onClick' | 'onFocus' | 'onBlur'

type ApiEndpoint = `/api/v1/${'users' | 'products' | 'orders'}`
// type: '/api/v1/users' | '/api/v1/products' | '/api/v1/orders'

Especially useful for event handler naming, API route typing, and CSS class generation.

never for Exhaustiveness Checking

// TypeScript will error if you forget to handle a case
function assertNever(value: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(value)}`)
}

type Shape = 'circle' | 'square' | 'triangle'

function getArea(shape: Shape, size: number): number {
  switch (shape) {
    case 'circle': return Math.PI * size ** 2
    case 'square': return size ** 2
    case 'triangle': return (Math.sqrt(3) / 4) * size ** 2
    default: return assertNever(shape) // Error if a case is missing
  }
}

If you add 'hexagon' to Shape and forget to add a case in the switch, TypeScript will error at the assertNever call. Zero runtime surprises.

Index Signatures Done Right

// ❌ Loose index signature, any string key is valid
interface Config {
  [key: string]: string
}

// ✅ Prefer Record with a union of known keys
type Config = Record<'host' | 'port' | 'database', string>

// Or use satisfies + as const for maximum precision
const config = {
  host: 'localhost',
  port: '5432',
  database: 'myapp',
} satisfies Record<string, string>

Index signatures are sometimes necessary, but when you know the possible keys upfront, Record with a union type gives you better autocomplete and stricter validation.


Final Thoughts

TypeScript's value isn't in the number of features you use. It's in using the right features precisely.

The eight tips in this article share a common thread: they're all about making the type system reflect the real constraints of your domain. Discriminated unions that model actual states. Utility types that derive from a single source of truth. unknown that forces you to verify before you use.

TypeScript that fights you is usually TypeScript that's been used loosely, any where there should be unknown, optional fields where there should be discriminated unions, manual types where there should be inference.

TypeScript that helps you is TypeScript that's been used precisely.

The difference isn't complexity. It's intention.


Which of these tips was new to you, and which one are you already using?

Drop it in the comments. And if there's a TypeScript pattern that's saved you hours in a real project, share it, the best tips in these threads always come from the comments.

If this was useful, a ❤️ or a 🦄 means a lot.
And follow along for the next article in the series.