Skip to content

02 · Basic Types

🎥 Video walkthrough

TypeScript's type system layers compile-time checks on top of JavaScript's existing runtime types. Most basic types read almost like English.

string, number, boolean

let username: string = "alice";
let age: number = 30;
let isActive: boolean = true;

// TypeScript infers types from the initializer even without an annotation
let city = "Seattle";   // inferred as string
// city = 42;           // error TS2322: Type 'number' is not assignable to type 'string'

Unlike some languages, TypeScript has a single number type for all numbers — no separate int/float/double. There's also bigint for arbitrarily large integers (const big: bigint = 123n;).

Arrays

Two equivalent syntaxes for typing arrays:

let scores: number[] = [90, 85, 77];
let names: Array<string> = ["Alice", "Bob"];   // generic syntax, same meaning

scores.push(100);     // fine -- number
// scores.push("A");  // error TS2345: Argument of type 'string' is not assignable
//                        to parameter of type 'number'.

T[] and Array<T> are identical — T[] is more common for simple element types, Array<T> reads more clearly when T itself is complex.

Tuples — fixed-length, mixed-type arrays

A tuple is an array with a known length and a known type at each position — useful for representing a fixed, small, ordered set of values:

let point: [number, number] = [10, 20];
let entry: [string, number, boolean] = ["Alice", 30, true];

console.log(point[0]);   // 10 -- typed as number
console.log(entry[0]);   // "Alice" -- typed as string
console.log(entry[1]);   // 30 -- typed as number

// entry[0] = 42;   // error -- position 0 must be string
// point.push(30);  // TypeScript allows this at compile time (a known tuple
//                     limitation) but it's discouraged -- avoid mutating tuples

Tuples with named positions (labels are documentation only, purely for readability — they don't change behavior):

let httpResponse: [status: number, body: string] = [200, "OK"];

any — opting out of type checking

any disables type checking entirely for that value — TypeScript will let you do anything with it, and won't catch mistakes:

let data: any = 42;
data = "now a string";      // fine, any allows reassignment to any type
data.toUpperCase();          // no error at compile time...
data();                      // ...even this "compiles" -- calling a number as a function!
// Runtime: TypeError: data is not a function

any is an escape hatch for gradually migrating JavaScript code or handling truly dynamic data — but overusing it defeats the entire purpose of TypeScript. Treat every any in a codebase as a small hole in your safety net.

unknown — the type-safe alternative to any

unknown also accepts any value, but forces you to narrow the type before doing anything with it — the safe way to handle values whose type you don't know yet (e.g. parsed JSON, user input):

let value: unknown = 42;

// value.toUpperCase();   // error TS18046: 'value' is of type 'unknown'.

if (typeof value === "number") {
  console.log(value.toFixed(2));   // fine -- narrowed to number inside this block
}

if (typeof value === "string") {
  console.log(value.toUpperCase());   // fine -- narrowed to string here instead
}

Type narrowing gets its own deep dive in Level 2, Module 9. For now: prefer unknown over any whenever a value's type is genuinely not known up front — it keeps the compiler checking your work instead of trusting you blindly.

null and undefined

let a: null = null;
let b: undefined = undefined;

let maybeName: string | null = null;   // union type -- either a string or null
maybeName = "Alice";                    // also fine

With strict mode on (the default recommendation, see Module 9), null and undefined are not automatically assignable to other types — you must explicitly include them in a union, as shown above. This single setting (strictNullChecks) eliminates an entire class of "cannot read property of undefined" runtime crashes by forcing you to handle the possibility at compile time.

void — functions that return nothing

function logMessage(message: string): void {
  console.log(message);
  // no return statement -- or a bare `return;` -- both fine for void
}

Type inference vs. explicit annotation

TypeScript infers types wherever it reasonably can — you don't need to annotate every single variable:

let count = 0;              // inferred: number
let label = "score";        // inferred: string
let items = [1, 2, 3];       // inferred: number[]

// Explicit annotations matter most for function parameters (TypeScript can't
// guess what a caller will pass) and for declaring intent on public APIs.

How It Actually Works

TypeScript's primitive types (string, number, boolean) aren't runtime-enforced containers — they're compile-time labels the checker attaches to expressions and then erases. When you write let age: number = 30, the annotation : number never survives to the emitted JavaScript; the line becomes just let age = 30;. This is why typeof age === "number" is a runtime check you still have to write yourself if the value could come from somewhere untyped (JSON, user input) — TypeScript's type system has zero presence at runtime to enforce it for you.

TypeScript is structurally typed, not nominally typed. When the checker decides whether a value of type A is assignable to a variable of type B, it doesn't check "was this declared as a B" — it compares the shape (the set of member names and their types). This matters even for primitives: literal types like "active" are structurally compared against their widened base type string, and the checker tracks two versions of a type for let vs const — a const gets the narrowest literal type ("active"), while let widens to the general primitive (string) at the point of declaration, because a let binding is presumed reassignable to any value of that base type.

any disables structural comparison entirely — it's a escape hatch where the checker stops comparing shapes and lets any operation through, effectively opting that value (and everything derived from it) out of static analysis. unknown keeps structural comparison active but starts from "no known members," forcing you to narrow (typeof, instanceof, a type guard) before the checker will let you use it — the compiler makes you prove the shape before it trusts you with member access.

Type inference works by the same structural machinery in reverse: for let x = 5, the checker evaluates the initializer's type (the literal 5) and widens it to number for a mutable binding, storing that as x's declared type for the rest of its scope — no annotation needed because the initializer already carried enough shape information.

Cheat sheet

Type Example Notes
string "hello" Text
number 42, 3.14 All numbers -- no int/float split
boolean true, false
T[] / Array<T> number[] Homogeneous array
[T, U] [string, number] Fixed-length tuple
any -- Disables checking -- avoid
unknown -- Safe "don't know yet" -- requires narrowing
null / undefined -- Only assignable elsewhere via a union under strict
void -- Function returns nothing meaningful

🔀 See this in another language

Exercise

Declare typed variables for a productName (string), price (number), inStock (boolean), and a tags array of strings. Then create a tuple priceRange: [number, number] for a min/max price. Write a function describeValue(value: unknown): string that uses typeof checks to return a different description string depending on whether value is a string, a number, a boolean, or none of those.