TypeScript compares object shapes, not declared names — a plain object with the right properties satisfies an interface with zero explicit relationship to it, confirmed by compiling exactly that against TypeScript 5.9.3. This structural model, not a nominal one, is the single biggest mental shift coming from languages like Java or C#.
1. Does TypeScript use structural or nominal typing?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
STRUCTURAL typing (not nominal): TS compares SHAPE, not declared
relationships. No "implements X" needed — matching properties
is all that's checked.
interface Point { x: number; y: number }
const obj = { x: 1, y: 2, extra: true };
logPoint(obj); // ✅ works — obj HAS x and y, extra property ignored
// (passing the LITERAL directly is stricter — see Interfaces)
INFERENCE-FIRST:
let count = 5; // ✅ inferred, no annotation needed
function add(a: number, b: number) { ... } // ⚠️ params NEED annotations
// (no value to infer FROM)
TYPES ARE FULLY ERASED at compile time:
.ts → tsc → plain .js, ZERO trace of types remain
→ ZERO runtime cost, ZERO runtime validation
(same fact confirmed for Node's native .ts execution — see
TypeScript in Node.js)
Annotate: function PARAMETERS (required) + PUBLIC API return types
(documentation/contract). Skip: obviously-inferred local variables.
The beginner framing: TypeScript is JavaScript with an added layer that checks, before your code ever runs, whether the values flowing through it match the shapes your code expects — catching a whole category of bugs (undefined is not a function, passing the wrong argument shape) at compile time instead of in production.
The precise mental model: TypeScript's type system is structural, not nominal. It compares the shape of a value — what properties it has and what types they are — not its declared name or where it was defined. This is the single biggest adjustment for anyone coming from a nominally-typed language like Java or C#, where a class must explicitly implements an interface to satisfy it.
interface Point { x: number; y: number }function logPoint(p: Point) { console.log(p.x, p.y);}// this object never mentions "Point" anywhere — no `implements`, no relationship declaredconst location = { x: 10, y: 20, label: "home" };logPoint(location); // works — the SHAPE matches, that's all that's checked
Confirmed by compiling this exact code against TypeScript 5.9.3: location satisfies Point purely because it has an x: number and a y: number — the extra label property and the total absence of any declared relationship to Point don't matter at all.
Inference-first: TypeScript figures out most types on its own#
let count = 5; // inferred as `number` — no annotation neededconst name = "Ada"; // inferred as the LITERAL type "Ada" (const can't be reassigned)const user = { id: 1, active: true }; // inferred as { id: number; active: boolean }
A common beginner habit is to annotate everything explicitly (let count: number = 5). This isn't wrong, but it's usually unnecessary — TypeScript's inference already produces the correct type from the assigned value. The more experienced convention is inference-first: let TypeScript infer types for local variables and let explicit annotations concentrate at boundaries — function parameters (which have no value to infer from) and exported/public APIs (where being explicit documents intent for other code reading it).
function add(a: number, b: number): number { // parameters NEED annotations — no value to infer from return a + b; // return type is ALSO inferable, the explicit ": number" here is optional}
function greet(name: string): string { return `Hello, ${name}`;}
// compiled output — every trace of the types is GONE:function greet(name) { return `Hello, ${name}`;}
TypeScript's types have zero runtime cost and zero runtime presence — tsc (or any TS-aware build tool) strips every type annotation, interface, and type alias during compilation, leaving plain JavaScript with no trace they ever existed. This is the same fact underlying Node's native .ts execution (type stripping, not type checking) — types are a compile-time-only tool for catching mistakes before code runs, not a runtime feature.
Yes, it compiles without error. admin is not a literal passed directly to greet() — it's a separate variable — so TypeScript checks it structurally: does it have a name: string and an age: number? Yes. The extra role property is simply irrelevant to whether admin satisfies User. (Passing the object literal directly, greet({ name: "Grace", age: 40, role: "admin" }), triggers a different, stricter check — covered in Interfaces.)
Write a function whose parameter type is inferred purely from how it's used, to internalize that TypeScript doesn't require annotating everything:
// no explicit return type annotation — TypeScript infers itfunction double(n: number) { return n * 2; // inferred return type: number}const values = [1, 2, 3].map(double); // inferred: number[]// contrast: a parameter has NOTHING to infer from without an annotationfunction triple(n) { // ❌ TypeScript (in strict mode) requires an annotation here
This demonstrates the actual rule precisely: inference works forward from values that already have a known type (return statements, array elements) — but a function parameter has no incoming value to infer from at the point it's declared, which is exactly why parameters specifically need explicit annotations even under an inference-first philosophy.
Structural typing directly explains behavior covered later in Interfaces (excess property checks on literals) and Type Aliases (why interfaces and aliases can often be used interchangeably despite being declared differently — TypeScript only cares about the resulting shape). And type erasure at compile time is the exact mechanism verified directly against this app's own Node.js runtime in the Node.js domain's native .ts execution topic — the same "types vanish before anything runs" fact, from two different angles.
class Dog { bark() {} }class Robot { bark() {} } // NOT a Dog, but...function makeBark(d: Dog) { d.bark(); }makeBark(new Robot()); // ✅ compiles — same shape, no "extends Dog" needed
Coming from a nominally-typed language, this often looks like a bug — but it's TypeScript's structural model working exactly as designed.
2. Over-annotating variables that are already clearly inferred#
const isActive: boolean = true; // ❌ redundant — inference already gets this exactly right
This isn't a compile error, just unnecessary noise — reserve explicit annotations for places inference genuinely can't help (function parameters) or where being explicit documents something for readers.
function isString(x: unknown): boolean { return typeof x === "string"; // the RUNTIME check — not the ": unknown" annotation}
The : unknown annotation itself does nothing at runtime — only actual runtime checks like typeof affect program behavior once compiled; the type annotation's entire job is done at compile time, then it's gone.
Let inference handle local variables; reserve explicit annotations for function parameters (required) and exported/public function return types (documents intent, even though often inferable).
Think in shapes, not class hierarchies — when designing a function's parameter type, ask "what properties does this code actually need?" rather than reaching for inheritance.
Don't expect types to validate anything at runtime — if a value's actual shape needs verifying at runtime (parsing JSON from an API, for instance), that requires actual runtime checks or a validation library, not a type annotation alone.
Since types are fully erased at compile time, they impose zero runtime performance cost — a heavily-typed function and an untyped equivalent compile to identical JavaScript and run identically fast.
tsc's type-checking work happens entirely at build/compile time, not per-request or per-execution — this is why type-checking cost (covered in depth once tooling comes up) is a build-pipeline concern, never a production runtime concern.