DEV Community

Timevolt
Timevolt

Posted on

The Matrix: How TypeScript's Unknown Type is Your Neo

The Quest Begins (The "Why")

I was knee‑deep in a refactor that felt like trying to navigate the Nebuchadnezzar’s hovercraft without a map. A teammate had poured a bunch of API responses into a generic any variable, and every time I tried to access a property I got that dreaded “Property 'foo' does not exist on type 'any'” warning silenced by a quick as any. The code compiled, but at runtime we kept hitting undefined because the shape of the payload changed without anyone noticing.

It was the classic “I thought I was safe, but I’m actually walking into a trap”. I needed a way to tell TypeScript: “Hey, I don’t know what this is yet, but I promise I’ll check before I use it.” That’s when I stumbled upon a few language features that most developers gloss over—yet they turned my shaky code into something as reliable as Neo dodging bullets in slow motion.

The Revelation (The Insight)

1. unknown – The Safer Cousin of any

any says, “I trust you, do whatever you want.” unknown says, “I don’t trust you yet—prove it first.” If a value is unknown, you must narrow its type before accessing any property or calling a method. The compiler forces you to write a guard, which eliminates the silent runtime bugs that any loves to hide.

2. never – The Exhaustiveness Watchdog

never represents a type that never occurs. It’s perfect for ensuring that a switch (or an if/else chain) has covered every possible case. If you forget a branch, TypeScript will infer never for the fallback and yell at you, turning a potential logic hole into a compile‑time error.

3. Template Literal Types – Building String Unions Like a Pro

TypeScript can now manipulate types as if they were strings. With template literal types you can generate unions of possible keys, event names, or even CSS class names directly from other types. No more hand‑crafted string arrays that fall out of sync with your code.

Wielding the Power (Code & Examples)

🎯 Before: The any Trap

// API response – we pretend we know the shape, but we really don’t
const rawResponse: any = await fetchUserData();

// Later in the code…
console.log(rawResponse.name.toUpperCase()); // ✅ compiles, but blows up if name is missing
Enter fullscreen mode Exit fullscreen mode

If the API ever returns {} or renames name to fullName, we get a runtime Cannot read property 'toUpperCase' of undefined.

✅ After: Using unknown

const rawResponse: unknown = await fetchUserData();

// We must check before we use it
if (typeof rawResponse === 'object' && rawResponse !== null && 'name' in rawResponse) {
  const name = (rawResponse as { name: string }).name;
  console.log(name.toUpperCase()); // safe!
} else {
  console.warn('Unexpected response shape');
}
Enter fullscreen mode Exit fullscreen mode

The compiler won’t let us skip the guard. If we forget it, we get an error: Object is of type 'unknown'.

🎯 Before: Missing a Switch Case

type UserRole = 'admin' | 'editor' | 'viewer';

function getRoleDescription(role: UserRole): string {
  switch (role) {
    case 'admin':
      return 'Full access';
    case 'editor':
      return 'Can edit content';
    // Oops! Forgot 'viewer'
    default:
      return 'Unknown role';
  }
}
Enter fullscreen mode Exit fullscreen mode

If a new role ('moderator') is added later, the function will silently return 'Unknown role'—a bug that only shows up in production.

✅ After: never to Enforce Exhaustiveness

function getRoleDescription(role: UserRole): string {
  switch (role) {
    case 'admin':
      return 'Full access';
    case 'editor':
      return 'Can edit content';
    case 'viewer':
      return 'Read‑only access';
    default:
      // The variable `exhaustiveCheck` is never reachable if all cases are covered
      const exhaustiveCheck: never = role;
      return exhaustiveCheck; // Type error if a case is missing
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, adding 'moderator' without handling it triggers: Type '"moderator"' is not assignable to type 'never'.

🎯 Before: Hard‑coded Event Names

const events = ['click', 'dblclick', 'mouseover'] as const;

function handleEvent(name: string, cb: () => void) {
  if (!events.includes(name as any)) {
    throw new Error(`Unsupported event: ${name}`);
  }
  // …register listener
}
Enter fullscreen mode Exit fullscreen mode

If we rename a DOM event in the spec or add a custom one, we have to remember to update this array—easy to forget.

✅ After: Template Literal Types + as const

type BaseEvent = 'click' | 'dblclick' | 'mouseover';
type PrefixedEvent = `ui:${BaseEvent}`; // "ui:click" | "ui:dblclick" | "ui:mouseover"

const uiEvents: readonly PrefixedEvent[] = [
  'ui:click',
  'ui:dblclick',
  'ui:mouseover',
] as const; // preserves literal types

function handleUiEvent<E extends PrefixedEvent>(name: E, cb: () => void) {
  // name is already guaranteed to be one of the allowed strings
  console.log(`Handling ${name}`);
  // …register listener
}

// Usage
handleUiEvent('ui:click', () => console.log('clicked!'));
// handleUiEvent('ui:keyup', () => {}); // ❌ Type error: '"ui:keyup"' is not assignable to type 'PrefixedEvent'
Enter fullscreen mode Exit fullscreen mode

If we ever add a new base event, we just extend BaseEvent and the union of prefixed events updates automatically—no manual array maintenance.

Why This New Power Matters

Mastering these three features feels like unlocking Neo’s ability to see the Matrix code: you start spotting hidden assumptions before they become runtime ghosts.

  • unknown forces you to write intentional type guards, turning sloppy any casts into deliberate, safe checks.
  • never turns your switch statements into self‑checking contracts—adding a new case means you must handle it, or the build fails.
  • Template literal types let you derive complex string unions directly from your existing types, eliminating drift between constants and usage.

Together, they make your codebase more resilient, your refactors less scary, and your teammates’ lives easier. You’ll spend fewer late‑night debugging sessions chasing undefined properties and more time building features that actually matter.

Your Turn – The Challenge

Pick a piece of code in your project that leans heavily on any or a hard‑coded string list. Replace it with unknown + a guard, or a never‑exhaustive switch, or a template literal type derived from a existing type. Drop the before/after snippet in the comments below and let’s see how many runtime bugs we can squash together!

Happy coding, and remember: the truth is out there—just make sure you check it before you use it. 🚀

Top comments (0)