TypeScript June 30, 2026 Aditya Rawas

TypeScript Types Deep Dive: Primitives, Literals, and Escape Hatches

In Part 2, we saw the TypeScript compiler strip type syntax at compile time — the : string, : User, the interface blocks, all gone. But what exactly are those types?

Before TypeScript can validate a single line of your code, it needs a type system to validate against.

This post covers that type system from the ground up: the 7 primitives TypeScript inherits from JavaScript, how type inference works, what literal types are, and the three special types that every TypeScript developer eventually has to understand — any, unknown, and never.


The 7 Primitive Types

TypeScript is built on JavaScript, so it starts with JavaScript’s 7 primitive value types. These are the atoms of the type system — every other type you’ll ever write is built from combinations of these.

Click through each type below to see what values it accepts, what the TypeScript compiler rejects, and what typeof returns for it at runtime:

string Text values of any length

Covers all text — single quotes, double quotes, and template literals. TypeScript knows every built-in string method (.toUpperCase, .split, .trim) on values typed as string.

example.ts TypeScript
let greeting: string = "Hello, World!";
let name = "Alice";           // inferred: string
let msg = `Hi ${name}!`;      // template literal: also string

// ❌ Error: Type 'number' is not assignable to type 'string'
greeting = 42;
Valid values
✓ "Hello"✓ "TypeScript"✓ `template literal`
Type errors
✗ 42✗ true✗ null
typeof at runtime
"string"

When a variable is initialized with a string value, TypeScript infers the type as string automatically. You rarely need to write : string on local variables.

A few things worth noting across all 7:

typeof isn’t always what you expect. The runtime typeof for null is "object" — a famous JavaScript bug from 1995 that TypeScript inherits. Never use typeof x === "object" to check for null; always use x === null directly.

TypeScript maps types to runtime values. When you write : string, TypeScript knows the value will be a JavaScript string at runtime. The type system mirrors JavaScript’s underlying value model — it doesn’t invent new runtime behaviors.

Strict mode matters for null and undefined. With strictNullChecks: true (enabled by the strict flag in tsconfig.json), null and undefined are not assignable to other types unless you explicitly include them in a union. This catches a huge class of runtime bugs. Always use strict mode.


Type Inference: TypeScript Reads Your Mind

You don’t need to write types everywhere. TypeScript watches what values you assign to variables and infers the type automatically.

Here’s the same add function in JavaScript versus TypeScript — see how TypeScript catches the mistake that JavaScript silently accepts:

// JavaScript — types are unknown until runtime
function add(a, b) {
return a + b;
}

add(1, 2);     // 3
add(1, "2");   // "12" — silent string concatenation!
add([], {});   // "[object Object]" — no error, wrong result

Notice that we didn’t annotate the return type on the TypeScript version in that example — TypeScript infers number because it sees two number parameters being added. Inference is working behind the scenes.

When to annotate, when to let TypeScript infer

A practical rule that experienced TypeScript developers use:

LocationAnnotate or infer?
Function parametersAlways annotate — TypeScript can’t infer what callers will pass
Function return typeAnnotate for public APIs, infer for private/small functions
Local variables with an initializerLet TypeScript inferconst x = 42 is cleaner than const x: number = 42
Class properties without initializerAnnotate — TypeScript can’t guess what you’ll set later

The goal is to write fewer type annotations, not more. Let TypeScript do the work wherever it can.


Literal Types: Narrow is More Precise

Here’s something that surprises most developers learning TypeScript: a type can be a specific value, not just a category of values.

string means “any string.” But "Alice" (with quotes) means “only the string Alice, nothing else.” These are called literal types.

Primitive type (wide)
string
accepts any string value
Literal type (narrow)
"Alice"
only accepts exactly "Alice"
"Alice"
"Bob"
"Hello, World!"
"TypeScript"
"any string works"
"Alice"
"Bob"
"Hello, World!"
"TypeScript"
"any string works"
let name: string = "Alice";
name = "Bob"; // ✓ OK
name = "any string"; // ✓ OK
let name: "Alice" = "Alice";
name = "Bob"; // ✗ Error!
name = "any string"; // ✗ Error!
Test it — type any string value and check assignability:
string
"Alice"

Literal types exist for all three of the primary primitives:

type Direction = "north" | "south" | "east" | "west"; // string literals
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;               // number literals
type Answer = true | false;                             // boolean literals (same as boolean)

The Direction type above only accepts exactly those four string values. Pass "northwest" and TypeScript rejects it immediately.

const vs let: the inference difference

This is where literal types get genuinely surprising. TypeScript infers different types for const and let even when they start with the same value:

let status = "active";   // inferred: string  (wide — can be reassigned)
const mode = "active";   // inferred: "active" (narrow — can never change)

With let, TypeScript widens the type to string because the variable could be reassigned to any string later. With const, it knows the value is fixed — so it infers the most precise type possible, the literal "active".

This matters in practice. If you write a function that expects "active" | "inactive" and you try to pass a let variable, TypeScript might reject it even though the value happens to be correct:

type Status = "active" | "inactive";

function setStatus(s: Status) { /* ... */ }

let s = "active";    // TypeScript infers: string (too wide!)
setStatus(s);        // ❌ Error: Argument of type 'string' is not assignable to type 'Status'

const s2 = "active"; // TypeScript infers: "active" (exactly right)
setStatus(s2);        // ✓ OK

Fix: use const, add an explicit type annotation (let s: Status = "active"), or use as const — which we’ll cover in Part 9.

What type does TypeScript infer for the variable x in: const x = 42 ?

The Special Three: any, unknown, and never

Beyond the 7 primitives, TypeScript has three special types that represent categories of values rather than specific JavaScript value kinds. These are the types you reach for when the primitive types aren’t enough — or when you need to handle values that TypeScript itself uses internally.

AVOID IN PRODUCTION
any Escape hatch — disables all type checking

any tells TypeScript to stop checking entirely. You can assign anything to it, read anything from it, and pass it anywhere. TypeScript goes silent. Useful when migrating a JavaScript codebase, but overuse defeats the point of TypeScript.

Capabilities
Assign any value to it
Assign it to any other type
Call it as a function
Access any property on it
TypeScript type checks active
Errors caught at compile time
example.ts any
let x: any = "hello";
x = 42;           // ✓ OK — any accepts anything
x = { a: 1 };    // ✓ OK

// TypeScript won't stop you — these silently crash at runtime:
x.toUpperCase();  // TS: OK  |  Runtime: crashes if x is a number
x();              // TS: OK  |  Runtime: crashes if x is not a function

// Spreads unsafety to other variables:
const n: number = x; // TS: OK  |  Runtime: x could be anything

// Rule of thumb: avoid any. Use unknown instead.

The practical guide: which one to use

Reach for unknown instead of any whenever you genuinely don’t know the type yet. Both accept all values, but unknown forces you to check before using — which means bugs get caught at compile time instead of production.

// ❌ any — compiles, crashes silently at runtime:
function processAny(data: any) {
  data.name.toUpperCase(); // TypeScript: OK. Runtime: possible crash.
}

// ✅ unknown — TypeScript catches the bug:
function processUnknown(data: unknown) {
  if (typeof data === "object" && data !== null && "name" in data) {
    const name = (data as { name: unknown }).name;
    if (typeof name === "string") {
      name.toUpperCase(); // TypeScript: OK. Runtime: safe.
    }
  }
}

never shows up in three places:

  1. Functions that always throw or loop forever — their return type is never because they never produce a value
  2. Exhaustiveness checks — when you’ve narrowed all possibilities away, TypeScript infers never, which lets you build compile-time checks that catch missing union cases
  3. Impossible intersectionsstring & number is never because no value can be both

You almost never write never directly. TypeScript infers it. But recognizing it when you see it is important — never on a variable usually means “you’ve already handled all the possible cases.”

You're writing a function that accepts the parsed body of an HTTP request. The body could be anything — object, array, string, or null. Which parameter type is safest?

What You’ve Learned

You now have a complete picture of TypeScript’s foundational type system:

  1. 7 primitive typesstring, number, boolean, null, undefined, symbol, bigint. TypeScript maps directly to JavaScript’s runtime value types.
  2. Type inference — TypeScript reads initializer values and infers types automatically. Annotate function parameters; let inference handle the rest.
  3. Literal types — A type can be a specific value ("active", 42, true). const infers literal types; let widens to the primitive.
  4. any — disables all checking. Use only during JS → TS migration.
  5. unknown — accepts all values, but forces narrowing before use. Always prefer over any.
  6. never — represents impossibility. Appears in unreachable code paths and exhaustiveness checks.

Next up in this series: Objects, Arrays, and Tuples — how to describe the shape of structured data in TypeScript.

Aditya Rawas

Written by

Aditya Rawas

Full-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.

Series

Learning Typescript

  1. 01 The Benefits of TypeScript
  2. 02 How TypeScript Compiles to JavaScript
  3. 03 TypeScript Types Deep Dive: Primitives, Literals, and Escape Hatches