The Structural Type System

TypeScript's type system has two properties that shape everything you do with it: it judges compatibility by structure rather than by name, and it infers types so you rarely have to spell them out. Add the small vocabulary of primitives, literal types, and the special types `any`, `unknown`, and `never`, and you have the foundation the rest of the language builds on.

The previous post introduced structural typing in passing. This post makes it precise and adds the rest of the base vocabulary: inference, the ways to name object shapes, and the three special types that trip up newcomers. Get these and the more advanced features (unions, generics, mapped types) become straightforward extensions.

Structural, not nominal

In a nominal type system (Java, C#), two types are compatible only if one explicitly declares it’s the other — the name is the identity. TypeScript is structural: two types are compatible if their shapes match, regardless of names.

interface Named { name: string; }
class Dog { constructor(public name: string) {} }

function greet(n: Named) { console.log(n.name); }
greet(new Dog("Rex"));            // ✓ Dog has a `name: string`, so it fits Named
greet({ name: "Ada", age: 3 });   // ✓ object literal with the right shape fits too

Nothing declared that Dog “is a” Named. It’s accepted because it has the structure of one. This is TypeScript matching JavaScript’s own duck-typed spirit — code that works if the object has the right properties — but checking it at compile time. The practical upshot: you type against shapes you need, not class hierarchies, which keeps code flexible and decoupled.

Type inference: let the compiler do the work

You rarely annotate everything, because TypeScript infers types from how values are used:

let count = 0;          // inferred: number
const name = "Ada";     // inferred: "Ada" (a literal type — see below)
const nums = [1, 2, 3]; // inferred: number[]
function double(n: number) { return n * 2; } // return type inferred: number

The guideline: annotate boundaries, infer internals. Put explicit types on function parameters and public API signatures (the contracts others depend on), and let inference handle local variables and return types where the intent is obvious. Over-annotating local variables adds noise the compiler already knows; under-annotating public functions removes the documentation and safety types are for. Good TypeScript reads mostly like clean JavaScript, with types concentrated at the edges.

Naming object shapes: interface vs. type alias

Two ways name object shapes, and they’re mostly interchangeable:

interface User { id: number; name: string; }
type UserT = { id: number; name: string };

A reasonable rule: use interface for object shapes you might extend or that model public contracts; use type for unions, tuples, function signatures, and anything computed. The difference matters less than consistency.

Primitives and literal types

TypeScript has the JavaScript primitives — string, number, boolean, null, undefined, bigint, symbol — plus arrays (number[] or Array<number>), tuples ([string, number]), and object types.

More interesting are literal types: a type can be a specific value, not just a category. "GET" is a type inhabited only by the string "GET". On their own they’re a curiosity; combined with unions (next post) they become powerful:

type Method = "GET" | "POST" | "PUT" | "DELETE";
function request(url: string, method: Method) { /* ... */ }
request("/x", "GET");   // ✓
request("/x", "PATCH"); // ✗ not one of the allowed literals

Literal types let you express “one of these exact values” precisely — safer and more self-documenting than a bare string, and the basis for discriminated unions later. This is worth noticing because const declarations infer literal types while let widens to the general type — a subtlety that matters when you build unions.

The three special types: any, unknown, never

Three types have no direct JavaScript analog and are the source of much confusion and misuse:

The any-vs-unknown distinction is a litmus test of TypeScript maturity: reaching for any discards the type system for that value; reaching for unknown keeps it and forces a check. Prefer unknown almost always.

Why this foundation matters

Structural typing makes TypeScript flexible and JavaScript-native — you type against shapes, not hierarchies. Inference keeps it low-friction — you annotate edges and let the compiler fill in the rest. Literal types add precision — “exactly these values.” And any/unknown/never give you the escape hatch, the safe-unknown, and the impossible-type that later features (narrowing, exhaustiveness, generics) build on. Everything more advanced in this series is a way of composing these basics into precise descriptions of your program’s shapes.

Key takeaways

Further reading

Sources & References

Structural typing explained