DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/TypeScript/TypeScript Basics
beginner20 min read·Updated Jun 2025

TypeScript Basics

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#.

typescriptstructural-typingtype-inferencecompile-time

Knowledge Check

15 questions · pass at 70%

0/15
easymcq

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.

Related topics

JavaScript FundamentalsTypes

On this page

20m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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 declared
const 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 needed
const 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
}

tsc vs. runtime: types are erased entirely#

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.


Try It#

Predict the outcome before checking the solution.

interface User { name: string; age: number }
 
function greet(user: User) {
  console.log(`Hi ${user.name}`);
}
 
const admin = { name: "Grace", age: 40, role: "admin" };
Solution

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.)


Implement It Yourself#

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 it
function 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 annotation
function 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.


Under the Hood#

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.


Common Mistakes#

1. Assuming TypeScript uses nominal (name-based) typing#

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.

3. Assuming types have any effect at runtime#

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.


Best Practices#

  • 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.

Performance Tips#

  • 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.

greet
(admin);
// does this compile?
return n * 3;
}