Chapter 10 of 24 · TypeScript for Beginners — Complete Free Tutorial

any, unknown and never

Most types in the TypeScript for Beginners series describe what a value is. Four special types instead describe how much the compiler is allowed to know: any switches checking off, unknown keeps it on without making promises, never says no value can exist, and void says a function returns nothing. This chapter of the 24-chapter series explains each one and when it belongs in your code.

any: turning the checker off

Declaring a value any is telling TypeScript to stop asking questions. Every operation is permitted, and the result flows onward unchecked:

let loose: any = 'hello';
console.log(loose.toUpperCase());
loose.doesNotExist();   // compiles fine, explodes at runtime

Expected output:

HELLO
TypeError: loose.doesNotExist is not a function

The danger is not the first line — it is the propagation. A value typed any can be assigned to a number without complaint, and whatever you compute from it is any too, so the checking hole spreads silently through your call graph. Reach for any only at the edges of a project: a third-party module without type definitions, or a legacy file you are migrating one step at a time — and always with a comment saying why.

Implicit any and noImplicitAny

The worst any is the one you did not write. Under strict mode — which this series assumes — the noImplicitAny setting refuses to guess for you:

function greet(name) {
  return `hi ${name}`;
}

Expected output: compilation stops with Parameter 'name' implicitly has an 'any' type. (TS7006). The fix is to supply a real type (name: string); writing name: any explicitly is allowed, but now it is a visible, reviewable decision rather than a silent default. The same rule applies to variables whose initializer gives the compiler nothing to work with.

unknown: the safe counterpart

unknown accepts every value exactly like any, but permits no operations until you have narrowed it. It is the right answer whenever you genuinely do not know what you are holding:

function label(value: unknown): string {
  if (typeof value === 'string') return value.toUpperCase();
  if (typeof value === 'number') return value.toFixed(2);
  if (Array.isArray(value)) return `list of ${value.length}`;
  return 'something else';
}

console.log(label('scriptly'));
console.log(label(7));
console.log(label([1, 2, 3]));
console.log(label(null));

Expected output:

SCRIPTLY
7.00
list of 3
something else

Read a property before narrowing and you get 'value' is of type 'unknown' (TS18046) — exactly the error you want. Two places where unknown already appears in your code: JSON.parse() returns any, so annotate the result (const data: unknown = JSON.parse(text)) to recover checking immediately, and under strict mode every catch variable since TypeScript 4.4 is unknown, so guard it before reading error.message.

never: the type of nothing

never describes values that never occur. You meet it in three situations, all of them useful:

  • Functions that always throw — their control flow never continues, so the declared return type is never.
  • Exhaustive checks — after a fully covered switch the value is never, which is how the assertNever guard from chapter 8 proved completeness.
  • Unreachable code — anything after a never-returning call cannot run, and editors flag it (with "allowUnreachableCode": false in tsconfig.json it becomes error TS7027, Unreachable code detected).
function explode(): never {
  throw new Error('boom');
}

try {
  explode();
  console.log('never printed');
} catch (error) {
  console.log('caught:', (error as Error).message);
}

Expected output:

caught: boom

Note that never is required, not optional, for this trick: a function annotated : void or one the compiler cannot prove always throws will not make the following lines unreachable.

void: the quiet neighbour

void means “this function returns nothing useful”. It is not the same as undefined, though void functions return undefined in practice:

function logAndReturn(value: string): void {
  console.log(value);
}

const nothing = logAndReturn('side effect');
console.log(nothing);

Expected output:

side effect
undefined

A function declared : void may not return a value (you would get TS2322), yet a function that returns a value is still accepted where () => void is expected — that is why you can pass () => 42 as an event handler. Use void for procedures and callbacks whose result you intend to ignore; chapter 18 returns to this when typing asynchronous code.

Comparing the four

TypeWhat it meansUse before checking?Where it belongs
anyAnything; checking is offYes — that is the dangerEscape hatch at a project boundary, sparingly
unknownAnything; checking is onNo — narrow firstJSON, user input, catch variables
neverNo value at allNot applicableAlways-throwing functions, exhaustive checks
voidNothing returnedNot applicableProcedures and ignored-result callbacks

Think of the axis this way: unknown is the top of the type hierarchy (everything is one) and never is the bottom (nothing is one), while any deliberately steps outside the hierarchy and void only describes a function's answer.

Common mistakes

  • Confusing silence with safety — code that compiles with any is not code that works; prefer unknown plus a guard.
  • Letting any leak out of a helper — anything computed from it becomes any too, so the hole spreads; annotate the return type explicitly.
  • Reading error.message in a catch block — the variable is unknown under strict mode; check error instanceof Error first.
  • Using never for “probably nothing” — never means no value can exist; a value that might be missing is undefined, not never.
  • Returning a value from a : void function — the declaration forbids it; drop the annotation or return nothing at all.

Practice checklist

  1. Take a function typed any, change it to unknown, and fix every error the compiler reports.
  2. Assign JSON.parse('{"a":1}') to an unknown variable and narrow it before reading a property.
  3. Write an explode(): never helper and confirm the line after it is flagged as unreachable.
  4. Add a default: return assertNever(state) clause to a discriminated union from chapter 8.
  5. Log the type of a catch variable under strict mode, then narrow it with instanceof Error.

What's next?

You can now describe any shape of value precisely — the next chapter makes types reusable with Generics.

Quiz: any, unknown and never

5 questions · 70% to pass

Frequently Asked Questions

TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.

You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.

No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.

If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.

Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.

strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.

any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.

Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.

Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.

Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.