Mert Özen Further With Every Line
Frontend TypeScript Basics — #04

Basic and Special Types in TypeScript: any, unknown, never and void

Mert Özen Nov 3, 2026 7 min 0 views
Basic and Special Types in TypeScript: any, unknown, never and void

Alongside string, number, boolean, bigint and symbol, we look at null and undefined in strict mode, why any is dangerous, how unknown makes it safe, and what void and never mean.

Version note: The code in this post targets TypeScript 7.0 and Node 26.

Predict First

Two functions, the same body, and the only difference is the parameter's type:

function trimSafe(input: unknown) {
  return input.trim();
}

function trimLoose(input: any) {
  return input.trim();
}

console.log(trimLoose(42));

At first glance unknown and any seem to say the same thing: "anything can go here." So which of these two functions compiles? And what happens when you call the one that compiles with a number, like on the last line? The answer comes once we've met both types.

Primitive Types: string, number, boolean

Each of the primitive values you know from the JavaScript series has a type of the same name in TypeScript. The ones you'll use most are string, number and boolean. They're all written in lowercase and match the results of JavaScript's typeof one to one.

let price = 19.99;

price = 20;
price = NaN;
price = Infinity;
// Error: Type 'string' is not assignable to type 'number'.
price = "20";

Something stands out here: TypeScript has no split between integers and decimals. Both 20 and 19.99 are number. So are NaN and Infinity; they're numbers in TypeScript just as they are in JavaScript. In other words, the number type doesn't promise you "this value is a valid price"; it only says "this is a number." The same goes for string: single quotes, double quotes or a template literal, it makes no difference. boolean takes only two values: true and false.

bigint and symbol

There are two less common primitives as well. bigint is for very large integers that number can't hold safely. You can see the difference in this line:

console.log(9007199254740993n, 9007199254740993);
// 9007199254740993n 9007199254740992

function addFee(amount: bigint, fee: number) {
  // Error: Operator '+' cannot be applied to types 'bigint' and 'number'.
  return amount + fee;
}

The first value is a bigint thanks to the trailing n, and it prints exactly as written. The second is a number; because it goes past JavaScript's safe integer limit, its last digit quietly changes. The error in the function reflects a JavaScript rule too: mix a bigint and a number and you get a TypeError at runtime. TypeScript shows you that error before the code runs. If you want to add them, you convert one explicitly: amount + BigInt(fee).

symbol, on the other hand, produces a unique value every time. When you write const id = Symbol("id"), TypeScript shows the type of id as unique symbol: no other symbol is equal to this value. Write it with let and the type is just symbol. We won't run into symbols much in the Basics series; it's enough to know they exist.

null, undefined and strictNullChecks

JavaScript has two ways of saying "no value": null and undefined. In TypeScript each has its own type, and in strict mode neither slips into any other type on its own:

let nickname: string = "ada";
// Error: Type 'null' is not assignable to type 'string'.
nickname = null;

let middleName: string | null = null;
middleName = "Lovelace";

If a variable really can be empty, you say so explicitly in its type: string | null means "a string or null." The setting that enforces this rule is called strictNullChecks, and it's part of strict. You see its real value in code like this:

const users = ["Ada", "Alan"];
const found = users.find((user) => user === "Grace");

// Error: 'found' is possibly 'undefined'.
console.log(found.toUpperCase());

if (found !== undefined) {
  console.log(found.toUpperCase());
}

console.log(found?.toUpperCase());
// undefined

When find doesn't find what it's looking for, it returns undefined. TypeScript knows this and infers the type of found as string | undefined. It won't let you call toUpperCase directly, because at runtime that line could blow up with "Cannot read properties of undefined." Check it with an if, or use optional chaining (?.), and the error goes away. TypeScript seeing found as a string inside the if block is called narrowing; it gets a post of its own.

If strictNullChecks were off, null and undefined could be assigned to any type, found would look like a plain string, and the error would only show up in production. This situation, perhaps the most common JavaScript bug of all, is the most valuable thing strict mode protects you from.

any: Where Type Checking Switches Off

any is how you tell TypeScript "don't check this value." With a value of type any you can do anything: read any property, call any method, assign it anywhere. TypeScript objects to none of it. That's why trimLoose from the opening compiles, and when it runs it blows up with this error:

TypeError: input.trim is not a function

any can show up even if you never write it yourself. Its best-known source is JSON.parse:

const data = JSON.parse('{"age": 36}');
// const data: any

const age = data.agee;
// const age: any

console.log(age.toFixed(1));

The return type of JSON.parse is any, because TypeScript can't know what will come out of the string. Nobody catches the agee typo, and on top of that age becomes any too. any is contagious: every value you derive from it is unchecked as well. The last line fails at runtime with "Cannot read properties of undefined (reading 'toFixed')." You opened a hole in the type system, and that hole keeps spreading through the code.

The noImplicitAny setting inside strict at least stops that hole from opening without you noticing. The "Parameter 'value' implicitly has an 'any' type." error we saw in the previous post comes from this setting: when TypeScript can't infer a type, instead of silently falling back to any it asks you for an annotation. You can still write any on purpose, but now it's a decision, not an accident.

unknown: The Safe any

unknown also says "anything can go here," but the outcome is the exact opposite: it requires you to prove what the value is before you use it. That's why trimSafe from the opening doesn't compile:

error TS18046: 'input' is of type 'unknown'.

The simplest way to prove it is the typeof you know from JavaScript:

function trimSafe(input: unknown) {
  if (typeof input === "string") {
    return input.trim();
  }
  return "";
}

console.log(`[${trimSafe("  Ada  ")}]`); // [Ada]
console.log(`[${trimSafe(42)}]`); // []

Inside the if block TypeScript knows input is a string and allows the trim call. Think of every value coming from outside this way: an API response, the contents of a file, something a user typed. Receive them as unknown instead of any and TypeScript forces you to write the checks. In the JSON.parse example, too, you can close the hole by writing const data: unknown = JSON.parse(...).

An analogy: any is a doorman who lets everyone in without asking for ID. unknown is a doorman who keeps everyone at the door and checks their ID. Anyone can walk up to either one, but only one of them knows who's inside.

void and never

You'll mostly meet these two types as function return types. void means "this function doesn't return a value meant to be used":

function logMessage(message: string): void {
  console.log(message);
}

const result = logMessage("Saved");
// const result: void

never means "this point is never reached." A function that always throws never returns normally; its return type is never:

function fail(message: string): never {
  throw new Error(message);
}

// Error: Type '1' is not assignable to type 'never'.
let impossible: never = 1;

never is an empty type that accepts no value at all. It looks useless at first, but in the narrowing post we'll use it to check whether a switch covers every possibility. A subtle point: if you leave the annotation off a function declaration like fail, TypeScript infers the return type as void, not never. If your intent is "this function never returns," write it out.

Common Pitfalls

The first is writing String, Number, Boolean with a capital letter. These aren't the primitive types; they're the types of JavaScript's wrapper objects. When TypeScript notices, its message says so plainly: "'string' is a primitive, but 'String' is a wrapper object. Prefer using 'string' when possible." Always use the lowercase ones.

The second is reaching for any to silence an error. The error disappears, but its cause stays where it was, and any leaves the code around it unchecked too. If you have a value whose type you genuinely don't know, use unknown and write the check.

The third is treating null and undefined as the same thing. TypeScript sees them as separate types: you can't assign undefined to a variable you declared as string | null. Decide which one your project uses to mean "empty" and be consistent.

A Small Experiment

★☆☆ Does it compile? let total: number = undefined; If it errors, how would you change the type so this variable can start out empty?

★★☆ After writing const value: unknown = "hello";, add the line value.length. Read the error, then fix it with a typeof check.

★★☆ Predict: if you don't annotate the return type of function fail(message: string) { throw new Error(message); }, which type does the editor show? What changes when you write the same function as an arrow function, const fail = (message: string) => { throw new Error(message); };?

★★★ Explain why the expression 10n + 5 doesn't compile, and fix it in two different ways: once so the result is a bigint, and once so it's a number.

We now know the types of individual values. But in real code values rarely travel alone; most of the time they sit inside a list. In the next post we'll look at array types, readonly arrays that can't be modified, and tuples, where every position carries its own type.