Phase 1 · Foundations · Lesson 1.2
BeginnerPrimitives, arrays, tuples and the special types
The building blocks of every TypeScript type: the seven primitives, arrays and tuples, any vs unknown vs never vs void, object vs {} vs Object, and why `as` is a loaded gun.
20 min
Every type you will ever write is built from a small set of pieces. Most bugs in interview questions hide in the odd ones: the difference between any and unknown, why a tuple still lets you push, why {} accepts a number. Get these right and the rest of the type system reads like plain English.
The primitives
JavaScript has seven primitive values, and TypeScript has a type for each. The type names are lowercase.
const title: string = "TypeScript";
const count: number = 42; // integers and floats: all one type
const done: boolean = false;
const huge: bigint = 9_007_199_254_740_993n; // needs target ES2020+
const id: symbol = Symbol("id");
const nothing: null = null;
const notSet: undefined = undefined;Three details interviewers like:
- There is no
intorfloat.numberis a 64-bit float, like JavaScript. For integers beyond2^53 - 1, usebigint. numberandbigintdon't mix.1n + 1is a type error, just as it's a runtimeTypeError.- Lowercase, not uppercase.
String,NumberandBooleanare the wrapper object types. You almost never want them.
const big = 10n;
// @ts-expect-error -- Operator '+' cannot be applied to types 'bigint' and 'number'.
const mixed = big + 1;
const fine = big + 1n;
// ^? const fine: bigintA special symbol: unique symbol
A const declared with Symbol() gets its own type, unique symbol, so TypeScript can tell two symbols apart. A let widens to plain symbol.
const key = Symbol("key");
// ^? const key: typeof key (a unique symbol)
let other = Symbol("other");
// ^? let other: symbolnull and undefined are their own types
With strictNullChecks (part of strict), null and undefined are not part of string or any other type. If a value can be missing, say so with a union:
let nickname: string | null = null;
nickname = "Ada";
// @ts-expect-error -- Type 'undefined' is not assignable to type 'string | null'.
nickname = undefined;Arrays: T[] and Array<T>
Two spellings, one type. string[] and Array<string> are identical; pick one and be consistent. Most codebases use T[] for simple types.
const names: string[] = ["Ada", "Linus"];
const ages: Array<number> = [36, 54];
const mixed = [1, "two", 3];
// ^? const mixed: (string | number)[]Note the parentheses in (string | number)[]. Without them, string | number[] means "a string, or an array of numbers". A common typo.
Readonly arrays
readonly string[] (or ReadonlyArray<string>) removes every mutating method: push, pop, splice, sort, index assignment. It's the right type for a parameter you only read.
function first(items: readonly string[]) {
// @ts-expect-error -- Property 'push' does not exist on type 'readonly string[]'.
items.push("x");
return items[0];
}
const list: string[] = ["a", "b"];
first(list); // fine: a mutable array can be passed where a readonly one is expected
const frozen: readonly string[] = ["a"];
// @ts-expect-error -- The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
const copy: string[] = frozen;The direction matters: mutable → readonly is fine, readonly → mutable is not, because the receiver could then mutate something you promised not to.
Tuples: arrays with a fixed shape
A tuple is an array where each position has its own type and the length is known.
const point: [number, number] = [10, 20];
const entry: [string, number] = ["age", 36];
const [key, value] = entry;
// ^? const value: number
// @ts-expect-error -- Type 'string' is not assignable to type 'number'.
const wrong: [string, number] = ["age", "36"];Optional, rest and labelled elements
// Optional element: must come after required ones; length is 1 | 2
type Range = [start: number, end?: number];
const open: Range = [5];
// Rest element: a string followed by any number of numbers
type Row = [label: string, ...values: number[]];
const row: Row = ["scores", 90, 85, 77];
// Rest can also sit at the start or in the middle
type Path = [...segments: string[], file: string];
const p: Path = ["src", "lib", "index.ts"];The labels (start, end) are documentation only. They show up in editor hints and when a tuple is used as a function's parameter list, but they don't change the type: [start: number] and [number] are the same type.
Tuple vs array inference
TypeScript never infers a tuple from an array literal on its own. You get an array unless you ask:
const a = [1, "a"];
// ^? const a: (string | number)[]
const b: [number, string] = [1, "a"]; // annotate
const c = [1, "a"] as const;
// ^? const c: readonly [1, "a"]as const gives you a readonly tuple of literal types. It's how you say "this exact value, frozen".
The tuple push gotcha
A plain tuple is still an array underneath, and TypeScript lets you call push on it:
const pair: [string, number] = ["a", 1];
pair.push(2); // compiles! length is now 3 at runtime, the type still says 2
const safePair: readonly [string, number] = ["a", 1];
// @ts-expect-error -- Property 'push' does not exist on type 'readonly [string, number]'.
safePair.push(2);If a tuple is meant to stay fixed, make it readonly.
Quick check
What is the type of t?
const t = ["x", 1];The special types: any, unknown, never, void
These four come up in almost every TypeScript interview.
any: the off switch
any turns type-checking off for that value. Anything can be assigned to it, and it can be assigned to anything, and every operation is allowed.
let loose: any = "hello";
loose.toFixed(2); // compiles, crashes at runtime
const n: number = loose; // compiles: any flows into everythingany is contagious: loose.foo.bar is also any, and so on. It's an escape hatch for migration, not a type.
unknown: the safe "anything"
unknown also accepts any value, but you can't do anything with it until you narrow it.
function describe(value: unknown): string {
// @ts-expect-error -- 'value' is of type 'unknown'.
value.toUpperCase();
if (typeof value === "string") {
return value.toUpperCase(); // narrowed to string
}
return String(value);
}
const u: unknown = 1;
// @ts-expect-error -- Type 'unknown' is not assignable to type 'number'.
const num: number = u;Use unknown for values from outside your program: JSON.parse results, catch variables (they are unknown under strict), anything from the network.
never: the empty type
never is the type with no values. Nothing is assignable to it (except never itself), and it's assignable to everything. You meet it in three places:
- A function that never returns (it always throws or loops forever).
- A variable in a branch that can't happen, after every case is ruled out.
- The result of impossible type operations, like
string & number.
type Shape = "circle" | "square";
function area(shape: Shape): number {
switch (shape) {
case "circle": return 3.14;
case "square": return 1;
default: {
const unreachable: never = shape; // compiles only if every case is handled
return unreachable;
}
}
}That default branch is the exhaustiveness check: add "triangle" to Shape and the assignment to never fails, pointing you at the switch you forgot.
void: "the return value doesn't matter"
void is the return type of a function that doesn't return anything useful. At runtime such a function returns undefined.
function log(message: string): void {
console.log(message);
}
const result = log("hi");
// ^? const result: voidvoid is not the same as undefined: it means "don't use this". The functions lesson shows why that difference matters for callbacks.
Quick check
Which line is a type error?
let a: any = 1;
let u: unknown = 1;
const s1: string = a;
const s2: string = u;object vs {} vs Object
Three similar spellings, three different meanings:
| Type | Accepts | Rejects |
|---|---|---|
object (lowercase) | any non-primitive: objects, arrays, functions | string, number, null, ... |
{} (empty object type) | everything except null and undefined, including 42 and "hi" | null, undefined |
Object (uppercase) | almost the same as {} | null, undefined |
const o1: object = { a: 1 };
// @ts-expect-error -- Type 'number' is not assignable to type 'object'.
const o2: object = 42;
const e1: {} = 42; // surprising but true: {} means "any non-nullish value"
const e2: {} = "hello";
// @ts-expect-error -- Type 'null' is not assignable to type '{}'.
const e3: {} = null;The {} surprise is a favourite: an empty object type describes "something with at least no properties", and every non-nullish value qualifies. If you mean "any object", write object. If you mean "any value", write unknown. Object differs from {} only in that it checks members against Object.prototype (so a toString returning a number is rejected); linters ban both for good reason.
A first look at literal types
A literal type is a single exact value used as a type. On its own it's rarely useful; in a union it becomes an enum-like set of allowed values.
type Direction = "up" | "down" | "left" | "right";
function move(dir: Direction) {}
move("up");
// @ts-expect-error -- Argument of type '"forward"' is not assignable to parameter of type 'Direction'.
move("forward");
let status = "idle"; // let widens: string
// ^? let status: string
const mode = "dark"; // const keeps the literal
// ^? const mode: "dark"Number and boolean literals work the same way: type Dice = 1 | 2 | 3 | 4 | 5 | 6, and boolean is literally true | false. Unions and narrowing get a full lesson later.
Type assertions: as and why it's dangerous
value as T tells the compiler "trust me, this is a T". It changes nothing at runtime. No conversion, no check.
interface User {
name: string;
email: string;
}
const user = {} as User; // compiles: you've lied to the compiler
user.email.toLowerCase(); // compiles, crashes: email is undefinedTypeScript does stop the most absurd assertions: the two types must overlap (one must be assignable to the other).
// @ts-expect-error -- Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other.
const n = "42" as number;
const forced = "42" as unknown as number; // the "double assertion" escape hatch
// ^? const forced: numberA double assertion through unknown is a code-review red flag: it silences the only safety check assertions have.
Safer alternatives, in order of preference:
- Annotate instead:
const user: User = {...}checks the value really is aUser. - Narrow with a runtime check (
typeof,in, a type guard). - Use
satisfies(later lesson) to check a value against a type without changing its inferred type.
The postfix ! is a close cousin: el! asserts "not null or undefined" and is equally unchecked.
Spot the error
This compiles, but it's unsafe in two places. Where, and what would you change?
interface Config {
port: number;
host: string;
}
function load(raw: string): Config {
return JSON.parse(raw) as Config;
}
const coords: [number, number] = [1, 2];
coords.push(3);Show the answer
JSON.parse(raw) as Config:JSON.parsereturnsany, and the assertion trusts whatever arrived. If the JSON is{},config.portisundefinedat runtime. Treat parsed data asunknownand check it.coords.push(3): mutable tuples still havepush. Make the tuplereadonly.
interface Config {
port: number;
host: string;
}
function isConfig(value: unknown): value is Config {
return (
typeof value === "object" && value !== null &&
"port" in value && typeof value.port === "number" &&
"host" in value && typeof value.host === "string"
);
}
function load(raw: string): Config {
const data: unknown = JSON.parse(raw);
if (!isConfig(data)) throw new Error("Invalid config");
return data;
}
const coords: readonly [number, number] = [1, 2];
// @ts-expect-error -- Property 'push' does not exist on type 'readonly [number, number]'.
coords.push(3);Try it
Play with the special types. Change unknown to any in parse and watch every error disappear, including the real bug.
function parse(json: string): unknown {
return JSON.parse(json);
}
const data = parse('{"items": [1, 2, 3]}');
// @ts-expect-error -- 'data' is of type 'unknown'.
data.items.length;
if (typeof data === "object" && data !== null && "items" in data && Array.isArray(data.items)) {
console.log(data.items.length); // narrowed: safe
}
const tuple = ["id", 7] as const;
// ^? const tuple: readonly ["id", 7]
const empty: {} = 0; // allowed: {} is "not null or undefined"▶ Try it in the TypeScript Playground
Recap
- Seven primitives:
string,number,boolean,bigint,symbol,null,undefined. Use lowercase names. T[]andArray<T>are the same type.readonly T[]removes mutating methods; mutable → readonly is allowed, not the reverse.- Tuples fix each position's type; they support optional (
?), rest (...) and labelled elements. Array literals infer as arrays; use an annotation oras constfor a tuple. - Mutable tuples still allow
push. Make fixed tuplesreadonly. anyturns checking off;unknownaccepts anything but must be narrowed;neverhas no values;voidmeans "ignore the return value".object= non-primitive,{}= anything butnull/undefined,Object≈{}.asis an unchecked promise to the compiler. Prefer annotations, narrowing orsatisfies.
Interview cards
1 / 7