AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

13 TypeScript preview / Lesson 1 of 5

What TypeScript adds — erased types, inference, and any versus unknown versus never

13 min · +10 XP · 2 exercises · 5 questions

Read the text13 min
Exercises0/2
Quiz5 q

Quick answer

What is the difference between any and unknown in TypeScript?

In TypeScript, any and unknown both accept every value, but any switches type checking off: every property access and call on it compiles, and whatever is derived from it is any too. unknown is the type-safe counterpart — nothing can be done with it until it is narrowed with typeof, instanceof, Array.isArray or a type guard. Use unknown for JSON, user input and catch variables.

TypeScript is JavaScript plus a static type system: annotations and inference that let a compiler check your program before it runs, then erase everything and emit plain JavaScript. Nothing about the runtime changes — no types exist at run time, no performance difference, the same engines — which is both the point and the trap: a type is a promise about a value, checked at compile time, and a value that arrives from JSON, a database or a user can break the promise without anyone noticing unless you check at the boundary. This lesson covers what the compiler does, the basic types and how inference assigns them, literal types and widening, and the three special types — any, unknown, never — whose correct use is most of the difference between TypeScript that helps and TypeScript that lies.

Erasure, and what that means

function total(items: { price: number; qty: number }[]): number {
  return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}

compiles to

function total(items) {
  return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}

tsc (or esbuild, swc, Babel — anything that strips types) removes the annotations. Consequences: a type error is a compile-time message, never a runtime exception; instanceof SomeInterface cannot exist; a value's runtime shape is whatever it actually is, whatever the annotation claimed. TypeScript's guarantee is conditional: if every value entering the program matches its declared type, then the program has no type errors. Making the "if" true at the boundaries (lesson 4) is your job.

Basic types and inference

let n: number = 3;            // annotation
let s = "hi";                 // inferred: string
let ok = true;                // boolean
let big = 10n;                // bigint
let nothing: null = null;     // null and undefined are types too
let ids: number[] = [];       // array of numbers — also Array<number>
let pair: [string, number] = ["a", 1];   // tuple: fixed length and per-position types
let maybe: string | undefined;           // union — one of several types
const point = { x: 1, y: 2 };            // inferred { x: number; y: number }

Inference assigns types from initialisers and return statements; annotate parameters (nothing to infer from), public function return types when you want the contract explicit, and variables declared without an initialiser. Over-annotating (const n: number = 3) adds noise; under-annotating parameters gives implicit any, which strict mode rejects.

Literal types and widening

const a = "left";             // type "left" — a literal type: exactly this string
let b = "left";               // type string — let widens, because it may be reassigned
let dir: "left" | "right" = "left";     // a union of literals — a lightweight enum
const cfg = { mode: "dark" };            // { mode: string } — widened inside an object
const cfg2 = { mode: "dark" } as const;  // { readonly mode: "dark" } — as const keeps literals and makes it readonly
const tuple = [1, 2] as const;           // readonly [1, 2]

Literal types are how TypeScript models "one of these exact values": "GET" | "POST", 200 | 404, true. as const is the tool for configuration objects and tuples that should keep their exact shape. Unions of string literals replace most enums (lesson 2).

any — the off switch

any is assignable to and from everything and every operation on it is allowed: const x: any = json; x.foo.bar() compiles. It is contagious — values derived from any are any — and it silently removes checking from every expression it touches. Legitimate uses: a migration step from JavaScript, the boundary of an untyped library, a genuinely dynamic value you will not inspect. In each case keep it local and convert to a real type as soon as possible. strict mode's noImplicitAny refuses implicit any (un-annotated parameters); explicit any is allowed but every occurrence should be a decision.

unknown — the type-safe top

unknown also accepts every value — but you can do nothing with it until you narrow it:

function handle(data: unknown) {
  data.length;                                  // error: Object is of type 'unknown'
  if (typeof data === "string") data.length;    // ok: narrowed to string
  if (Array.isArray(data)) data.map(String);    // ok: any[]
  if (typeof data === "object" && data !== null && "id" in data) …   // narrowed to object with id
}

JSON.parse returns any in the standard library — the first thing to do with it is as unknown (or a validator that returns a typed value), so that the compiler forces the checks. catch (err) binds unknown under useUnknownInCatchVariables (part of strict), which is why err.message needs err instanceof Error first. Rule: unknown at boundaries, never any.

never — the empty type

never has no values. It is the return type of a function that never returns (throw, infinite loop), the type of a variable after every possibility has been narrowed away, and the tool for exhaustiveness:

type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number };
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.s ** 2;
    default: { const exhaustive: never = s; throw new Error(`unhandled ${exhaustive}`); }
  }
}

Add a third shape and forget a case: s in the default is no longer never, and the assignment fails to compile — the compiler tells you every place that must be updated. never in a union disappears (string | never is string); a function returning never lets the compiler know code after it is unreachable.

void, object, Function

void is a return type meaning "nothing useful returned" (callers may not use the value); object is any non-primitive; Function is any callable (too loose — write the signature). Prefer undefined returns typed as void and specific object shapes over object.

Types in JavaScript files

The same checker runs on .js with // @ts-check at the top (or checkJs in tsconfig) using JSDoc annotations:

// @ts-check
/** @param {number} a @param {number} b @returns {number} */
function add(a, b) { return a + b; }
/** @type {{ name: string; tags: string[] }} */
const user = { name: "Ada", tags: [] };

This is the zero-build way to get most of TypeScript's checking in a JavaScript project, and the usual first step of a migration.

Common mistakes

  • Trusting an annotation on data that came from outside (const u = JSON.parse(text) as User — a lie the compiler cannot catch).
  • any to silence an error instead of understanding it; any spreading through a codebase.
  • Annotating everything (const n: number = 1) or nothing (implicit any parameters).
  • Forgetting as const on config objects, then fighting string where "dark" | "light" was meant.
  • Missing the exhaustive never check on a discriminated union.
  • Believing types add runtime safety — they add compile-time checking only.

Interview angle

  • "What does TypeScript add, and what happens at run time?" Static types checked at compile time, then erased; the emitted JavaScript has no types.
  • "any versus unknown?" Both accept anything; any disables checking on use, unknown requires narrowing before use.
  • "What is never for?" The empty type: functions that do not return, exhausted unions, exhaustiveness checks.
  • "const a = 'x' versus let a = 'x'?" "x" (literal) versus string (widened); as const prevents widening in objects and arrays.
  • "How do you type-check plain JavaScript?" // @ts-check with JSDoc annotations, or checkJs.

Key takeaways

  • Types are compile-time promises, erased at build; the runtime is plain JavaScript — check values at the boundaries.
  • Annotate parameters and public returns; let inference do the rest; unions and tuples describe shapes precisely.
  • const gives literal types, let widens, as const freezes shapes into readonly literals.
  • any disables checking (keep it local and rare); unknown forces narrowing (use it at boundaries); never marks impossibility and powers exhaustiveness.
  • // @ts-check + JSDoc brings the checker to JavaScript files.

Common questions

What is the never type in TypeScript?

never is the empty type: no value has it. It is the return type of a function that always throws or never finishes, the type left after every union member has been narrowed away, and the tool for exhaustiveness checks — assigning the leftover to a never variable fails to compile when a case is missing.

Do TypeScript types exist at run time?

No. The compiler checks the types and then erases them, so the emitted JavaScript contains no annotations and runs exactly as plain JavaScript would. A type error is a compile-time message, never a runtime exception, which is why data from JSON or users must be validated at the boundary.

Why does const infer a literal type but let infers string?

const a = "left" can never be reassigned, so TypeScript gives it the literal type "left". let b = "left" may be reassigned later, so its type widens to string. Literals inside objects and arrays widen too, unless you add as const.

What does as const do in TypeScript?

as const stops widening: every literal keeps its exact type, object properties become readonly and arrays become readonly tuples. { mode: "dark" } as const has the type { readonly mode: "dark" } rather than { mode: string } — the tool for configuration objects and fixed tuples.

How do you type-check a plain JavaScript file?

Add // @ts-check at the top of the file, or set checkJs in tsconfig, and write the types in JSDoc comments such as /** @param {number} a */. The TypeScript checker then runs over the .js file with no build step — the usual first step of a migration.

Mark as readSign in to keep your place.

TypeScript any vs unknown vs never, and Type Inference

TypeScript checks types at compile time, then erases them. How inference, literal types and as const work, and when to use any, unknown or never.

What is the difference between any and unknown in TypeScript?

In TypeScript, any and unknown both accept every value, but any switches type checking off: every property access and call on it compiles, and whatever is derived from it is any too. unknown is the type-safe counterpart — nothing can be done with it until it is narrowed with typeof, instanceof, Array.isArray or a type guard. Use unknown for JSON, user input and catch variables.

Lesson

TypeScript is JavaScript plus a static type system: annotations and inference that let a compiler check your program before it runs, then erase everything and emit plain JavaScript. Nothing about the runtime changes — no types exist at run time, no performance difference, the same engines — which is both the point and the trap: a type is a promise about a value, checked at compile time, and a value that arrives from JSON, a database or a user can break the promise without anyone noticing unless you check at the boundary. This lesson covers what the compiler does, the basic types and how inference assigns them, literal types and widening, and the three special types — any, unknown, never — whose correct use is most of the difference between TypeScript that helps and TypeScript that lies.

Erasure, and what that means

function total(items: { price: number; qty: number }[]): number {
  return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}

compiles to

function total(items) {
  return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}

tsc (or esbuild, swc, Babel — anything that strips types) removes the annotations. Consequences: a type error is a compile-time message, never a runtime exception; instanceof SomeInterface cannot exist; a value's runtime shape is whatever it actually is, whatever the annotation claimed. TypeScript's guarantee is conditional: if every value entering the program matches its declared type, then the program has no type errors. Making the "if" true at the boundaries (lesson 4) is your job.

Basic types and inference

let n: number = 3;            // annotation
let s = "hi";                 // inferred: string
let ok = true;                // boolean
let big = 10n;                // bigint
let nothing: null = null;     // null and undefined are types too
let ids: number[] = [];       // array of numbers — also Array<number>
let pair: [string, number] = ["a", 1];   // tuple: fixed length and per-position types
let maybe: string | undefined;           // union — one of several types
const point = { x: 1, y: 2 };            // inferred { x: number; y: number }

Inference assigns types from initialisers and return statements; annotate parameters (nothing to infer from), public function return types when you want the contract explicit, and variables declared without an initialiser. Over-annotating (const n: number = 3) adds noise; under-annotating parameters gives implicit any, which strict mode rejects.

Literal types and widening

const a = "left";             // type "left" — a literal type: exactly this string
let b = "left";               // type string — let widens, because it may be reassigned
let dir: "left" | "right" = "left";     // a union of literals — a lightweight enum
const cfg = { mode: "dark" };            // { mode: string } — widened inside an object
const cfg2 = { mode: "dark" } as const;  // { readonly mode: "dark" } — as const keeps literals and makes it readonly
const tuple = [1, 2] as const;           // readonly [1, 2]

Literal types are how TypeScript models "one of these exact values": "GET" | "POST", 200 | 404, true. as const is the tool for configuration objects and tuples that should keep their exact shape. Unions of string literals replace most enums (lesson 2).

any — the off switch

any is assignable to and from everything and every operation on it is allowed: const x: any = json; x.foo.bar() compiles. It is contagious — values derived from any are any — and it silently removes checking from every expression it touches. Legitimate uses: a migration step from JavaScript, the boundary of an untyped library, a genuinely dynamic value you will not inspect. In each case keep it local and convert to a real type as soon as possible. strict mode's noImplicitAny refuses implicit any (un-annotated parameters); explicit any is allowed but every occurrence should be a decision.

unknown — the type-safe top

unknown also accepts every value — but you can do nothing with it until you narrow it:

function handle(data: unknown) {
  data.length;                                  // error: Object is of type 'unknown'
  if (typeof data === "string") data.length;    // ok: narrowed to string
  if (Array.isArray(data)) data.map(String);    // ok: any[]
  if (typeof data === "object" && data !== null && "id" in data) …   // narrowed to object with id
}

JSON.parse returns any in the standard library — the first thing to do with it is as unknown (or a validator that returns a typed value), so that the compiler forces the checks. catch (err) binds unknown under useUnknownInCatchVariables (part of strict), which is why err.message needs err instanceof Error first. Rule: unknown at boundaries, never any.

never — the empty type

never has no values. It is the return type of a function that never returns (throw, infinite loop), the type of a variable after every possibility has been narrowed away, and the tool for exhaustiveness:

type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number };
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.s ** 2;
    default: { const exhaustive: never = s; throw new Error(`unhandled ${exhaustive}`); }
  }
}

Add a third shape and forget a case: s in the default is no longer never, and the assignment fails to compile — the compiler tells you every place that must be updated. never in a union disappears (string | never is string); a function returning never lets the compiler know code after it is unreachable.

void, object, Function

void is a return type meaning "nothing useful returned" (callers may not use the value); object is any non-primitive; Function is any callable (too loose — write the signature). Prefer undefined returns typed as void and specific object shapes over object.

Types in JavaScript files

The same checker runs on .js with // @ts-check at the top (or checkJs in tsconfig) using JSDoc annotations:

// @ts-check
/** @param {number} a @param {number} b @returns {number} */
function add(a, b) { return a + b; }
/** @type {{ name: string; tags: string[] }} */
const user = { name: "Ada", tags: [] };

This is the zero-build way to get most of TypeScript's checking in a JavaScript project, and the usual first step of a migration.

Common mistakes

  • Trusting an annotation on data that came from outside (const u = JSON.parse(text) as User — a lie the compiler cannot catch).
  • any to silence an error instead of understanding it; any spreading through a codebase.
  • Annotating everything (const n: number = 1) or nothing (implicit any parameters).
  • Forgetting as const on config objects, then fighting string where "dark" | "light" was meant.
  • Missing the exhaustive never check on a discriminated union.
  • Believing types add runtime safety — they add compile-time checking only.

Interview angle

  • "What does TypeScript add, and what happens at run time?" Static types checked at compile time, then erased; the emitted JavaScript has no types.
  • "any versus unknown?" Both accept anything; any disables checking on use, unknown requires narrowing before use.
  • "What is never for?" The empty type: functions that do not return, exhausted unions, exhaustiveness checks.
  • "const a = 'x' versus let a = 'x'?" "x" (literal) versus string (widened); as const prevents widening in objects and arrays.
  • "How do you type-check plain JavaScript?" // @ts-check with JSDoc annotations, or checkJs.

Key takeaways

  • Types are compile-time promises, erased at build; the runtime is plain JavaScript — check values at the boundaries.
  • Annotate parameters and public returns; let inference do the rest; unions and tuples describe shapes precisely.
  • const gives literal types, let widens, as const freezes shapes into readonly literals.
  • any disables checking (keep it local and rare); unknown forces narrowing (use it at boundaries); never marks impossibility and powers exhaustiveness.
  • // @ts-check + JSDoc brings the checker to JavaScript files.

Common questions

What is the never type in TypeScript?

never is the empty type: no value has it. It is the return type of a function that always throws or never finishes, the type left after every union member has been narrowed away, and the tool for exhaustiveness checks — assigning the leftover to a never variable fails to compile when a case is missing.

Do TypeScript types exist at run time?

No. The compiler checks the types and then erases them, so the emitted JavaScript contains no annotations and runs exactly as plain JavaScript would. A type error is a compile-time message, never a runtime exception, which is why data from JSON or users must be validated at the boundary.

Why does const infer a literal type but let infers string?

const a = "left" can never be reassigned, so TypeScript gives it the literal type "left". let b = "left" may be reassigned later, so its type widens to string. Literals inside objects and arrays widen too, unless you add as const.

What does as const do in TypeScript?

as const stops widening: every literal keeps its exact type, object properties become readonly and arrays become readonly tuples. { mode: "dark" } as const has the type { readonly mode: "dark" } rather than { mode: string } — the tool for configuration objects and fixed tuples.

How do you type-check a plain JavaScript file?

Add // @ts-check at the top of the file, or set checkJs in tsconfig, and write the types in JSDoc comments such as /** @param {number} a */. The TypeScript checker then runs over the .js file with no build step — the usual first step of a migration.

Exercises

What would TypeScript infer?

Each line is a declaration: const x = 5, let y = "hi", const arr = [1, "a"], const t = [1, 2] as const, const o = { "mode": "dark", "n": 1 }, let u. Using the starter's typeOf(value, { literal, readonly }), print <name>: <type> following TypeScript's rules: const keeps a primitive literal type (5, "a"), let widens it (number, string); array elements always widen and join into a union in the order string, number, boolean, null, undefined; object properties widen unless as const, which keeps literals, marks properties readonly and turns arrays into readonly tuples; a declaration with no initialiser is any; an empty array is never[].

Example →

x: 5
y: string
arr: (string | number)[]
t: readonly [1, 2]
o: { mode: string; n: number }
o2: { readonly mode: "dark" }
u: any   (declared without an initialiser: implicit any)

unknown at the boundary

Each line is JSON — treat the parsed value as unknown and narrow it step by step in handle(data): null → null: nothing to do; a string → string: <length> chars, upper=<UPPER>; a number → number: <2dp> or number: not finite; a boolean → boolean: yes|no; an array → array: <n> items, first=<JSON>; an object with a kind → dispatch circle (Shape circle: area=<2dp>) and square (Shape square: area=<n>) with assertNever in the default (it throws unhandled value: <JSON>); any other object → object: keys=<comma list or (none)>. Print narrowed to <result>, or error: invalid JSON / error: <message>.

Example: "hello", 42, {"kind":"circle","r":2}, {"kind":"hexagon","sides":6}, not json →

narrowed to string: 5 chars, upper=HELLO
narrowed to number: 42.00
narrowed to Shape circle: area=12.57
error: unhandled value: {"kind":"hexagon","sides":6}
error: invalid JSON

In this module: TypeScript preview

← Checkpoint — The DOM and the browser · Object types, interfaces, structural typing and discriminated unions →