Skip to main content
TypeScript Type System

TypeScript Fundamentals

TypeScript adds a static type system on top of JavaScript. Understanding types is the foundation of everything else in TypeScript.

How TypeScript Works: Transpilation

TypeScript is not directly executed. Browsers and Node.js only understand JavaScript. TypeScript must be transpiled (compiled) to JavaScript before it can run.

The TypeScript Compilation Pipeline

StageWhat HappensCan You See It?
LexingSource code is broken into tokensInternal
ParsingTokens become an Abstract Syntax Tree (AST)ts.createSourceFile()
Type CheckingTypes are validated, errors reportedYour IDE red squiggles!
EmittingAST is transformed to JavaScriptThe .js files

Type Erasure

Here’s the key insight: Types exist only at compile time. They are completely erased in the output JavaScript.
// TypeScript (input)
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const user: { id: number; name: string } = {
  id: 1,
  name: 'Alice'
};
// JavaScript (output) - Types are gone!
function greet(name) {
  return `Hello, ${name}!`;
}

const user = {
  id: 1,
  name: 'Alice'
};
Runtime Implications: Since types are erased, you cannot use TypeScript types for runtime checks. You can’t do if (typeof x === "string") based on a TypeScript string type—that’s just JavaScript’s typeof.

The TypeScript Compiler (tsc)

The tsc command is the TypeScript compiler:
# Compile a single file
tsc hello.ts          # Outputs hello.js

# Compile with options
tsc --target ES2020 --module ESNext hello.ts

# Watch mode (recompile on changes)
tsc --watch

# Initialize a new project with tsconfig.json
tsc --init

# Compile entire project (uses tsconfig.json)
tsc

Compile Time vs Runtime Errors

Error TypeWhen CaughtExample
Compile-timeBefore code runsType mismatch, missing properties
RuntimeWhile code runsNetwork failures, user input
// Compile-time error (caught by TypeScript)
let name: string = 42; // ❌ Type 'number' is not assignable to 'string'

// Runtime error (NOT caught by TypeScript)
const data = JSON.parse(userInput); // Might throw if invalid JSON
Why This Matters: TypeScript’s value is catching bugs BEFORE your code runs. But it can’t protect you from external data (APIs, user input)—you still need runtime validation for that.

1. Type Annotations

Type annotations explicitly declare the type of a variable, parameter, or return value.
// Variable annotations
let name: string = 'Alice';
let age: number = 25;
let isActive: boolean = true;

// The annotation is after the variable name, separated by :
let count: number;  // Declared but not initialized
count = 42;         // Assigned later

Why Annotate?

let name: string = 'Alice';
name = 42; // ❌ Error: Type 'number' is not assignable to type 'string'
TypeScript catches type mismatches at compile time, not runtime.

2. Type Inference

TypeScript is smart. It can infer types from the value you assign.
// TypeScript infers these types automatically
let name = 'Alice';     // string (inferred)
let age = 25;           // number (inferred)
let isActive = true;    // boolean (inferred)

name = 42; // ❌ Error: Still type 'string'!
Best Practice: Let TypeScript infer types when the type is obvious from the value. Add explicit annotations when:
  • The type isn’t obvious
  • You’re declaring without initializing
  • You want to be explicit for documentation

When to Annotate vs Infer

// Let TypeScript infer (cleaner)
const user = { name: 'Alice', age: 25 };

// Explicit annotation (more control)
const user: { name: string; age: number } = { name: 'Alice', age: 25 };

// Always annotate function parameters
function greet(name: string): string {
  return `Hello, ${name}!`;
}

3. Primitive Types

TypeScript has the same primitive types as JavaScript, plus a few extras.

Basic Primitives

// string
let firstName: string = 'Alice';
let greeting: string = `Hello, ${firstName}`;

// number (integers and floats)
let age: number = 25;
let price: number = 19.99;
let hex: number = 0xff;
let binary: number = 0b1010;

// boolean
let isLoggedIn: boolean = true;
let hasAccess: boolean = false;

// null and undefined
let nothing: null = null;
let notDefined: undefined = undefined;

// symbol
let id: symbol = Symbol('id');

// bigint
let bigNumber: bigint = 9007199254740993n;

Special Types

// any - Opt out of type checking (avoid!)
let anything: any = 'hello';
anything = 42;
anything = { foo: 'bar' };

// unknown - Type-safe alternative to any
let value: unknown = 'hello';
// value.toUpperCase(); // ❌ Error: Object is of type 'unknown'
if (typeof value === 'string') {
  console.log(value.toUpperCase()); // ✅ OK after type guard
}

// void - Function returns nothing
function log(message: string): void {
  console.log(message);
}

// never - Function never returns (throws or infinite loop)
function throwError(message: string): never {
  throw new Error(message);
}
Avoid any! It defeats the purpose of TypeScript. Use unknown if you truly don’t know the type, then narrow it with type guards.

4. Arrays

Arrays can be typed in two ways:
// Using Type[]
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ['Alice', 'Bob', 'Charlie'];

// Using Array<Type> (generic syntax)
let scores: Array<number> = [100, 95, 88];

// Mixed types? Use union
let mixed: (string | number)[] = [1, 'two', 3, 'four'];

// Readonly arrays
const readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // ❌ Error: Property 'push' does not exist

Array Methods with Types

const numbers = [1, 2, 3, 4, 5];

// map - TypeScript infers the return type
const doubled = numbers.map(n => n * 2); // number[]

// filter - Returns same type
const evens = numbers.filter(n => n % 2 === 0); // number[]

// find - Returns Type | undefined
const found = numbers.find(n => n > 3); // number | undefined

// reduce - Explicit accumulator type
const sum = numbers.reduce((acc, n) => acc + n, 0); // number

5. Tuples

Tuples are fixed-length arrays with specific types at each position.
// Define a tuple
let person: [string, number] = ['Alice', 25];

// Access elements (typed!)
const name = person[0]; // string
const age = person[1];  // number

// Error on wrong type
person[0] = 42; // ❌ Error: Type 'number' is not assignable to type 'string'

// Error on extra elements (in strict mode)
person = ['Bob', 30, true]; // ❌ Error: Source has 3 elements but target allows only 2

Labeled Tuples (TS 4.0+)

type UserTuple = [name: string, age: number, isAdmin: boolean];

const user: UserTuple = ['Alice', 25, true];

// Labels are just for documentation, access is still by index
const name = user[0]; // string

Optional Tuple Elements

type Response = [number, string, object?];

const success: Response = [200, 'OK'];
const withData: Response = [200, 'OK', { user: 'Alice' }];

6. Enums

Enums define a set of named constants.

Numeric Enums

enum Direction {
  Up,     // 0
  Down,   // 1
  Left,   // 2
  Right   // 3
}

let move: Direction = Direction.Up;
console.log(move);             // 0
console.log(Direction[0]);     // 'Up' (reverse mapping)

// Custom values
enum Status {
  Pending = 1,
  Active = 2,
  Inactive = 3
}

String Enums

enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE'
}

let favorite: Color = Color.Blue;
console.log(favorite); // 'BLUE'

const Enums (Inlined at compile time)

const enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500
}

const status = HttpStatus.OK; // Compiled to: const status = 200;
Modern Alternative: Many developers prefer union types over enums for better tree-shaking and simpler code:
type Direction = 'up' | 'down' | 'left' | 'right';

7. Object Types

Define the shape of objects with inline types or type aliases.

Inline Object Types

// Inline type annotation
let user: { name: string; age: number } = {
  name: 'Alice',
  age: 25
};

// Optional properties with ?
let config: { debug?: boolean; timeout: number } = {
  timeout: 3000
  // debug is optional
};

// Readonly properties
let point: { readonly x: number; readonly y: number } = { x: 10, y: 20 };
// point.x = 5; // ❌ Error: Cannot assign to 'x' because it is a read-only property

Type Aliases

Create reusable type definitions.
type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

const alice: User = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  isActive: true
};

const bob: User = {
  id: 2,
  name: 'Bob',
  email: '[email protected]',
  isActive: false
};

8. Union Types

A value can be one of several types.
// Basic union
let id: string | number;
id = 'abc123';
id = 123;

// Function with union parameter
function printId(id: string | number): void {
  console.log(`ID: ${id}`);
}

printId('abc');
printId(123);

Narrowing Union Types

function printId(id: string | number): void {
  if (typeof id === 'string') {
    // TypeScript knows id is string here
    console.log(id.toUpperCase());
  } else {
    // TypeScript knows id is number here
    console.log(id.toFixed(2));
  }
}

Literal Types

// Specific string values
type Status = 'pending' | 'active' | 'inactive';

let userStatus: Status = 'active';
// userStatus = 'unknown'; // ❌ Error: Type '"unknown"' is not assignable

// Specific numbers
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function roll(): DiceRoll {
  return Math.ceil(Math.random() * 6) as DiceRoll;
}

9. Type Assertions

Tell TypeScript you know better about the type.
// as syntax (preferred)
const input = document.getElementById('username') as HTMLInputElement;
input.value = 'Alice';

// Angle bracket syntax (not in JSX)
const input2 = <HTMLInputElement>document.getElementById('password');

// Double assertion (escape hatch - use sparingly!)
const x = 'hello' as unknown as number; // ⚠️ Dangerous!

Non-null Assertion

function getLength(value: string | null): number {
  // Tell TypeScript value is definitely not null
  return value!.length; // ⚠️ Use only when you're certain
}

// Better approach: actually check
function getLengthSafe(value: string | null): number {
  if (value === null) {
    return 0;
  }
  return value.length;
}
Use assertions sparingly! They override TypeScript’s type checking. If you’re wrong, you’ll get runtime errors. Prefer type guards for safety.

10. Type Narrowing

Narrow types using conditions and type guards.

typeof Guard

function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  return value * 2;
}

Truthiness Narrowing

function printName(name: string | null | undefined) {
  if (name) {
    console.log(name.toUpperCase()); // name is string
  } else {
    console.log('No name provided');
  }
}

instanceof Guard

function logDate(date: Date | string) {
  if (date instanceof Date) {
    console.log(date.toISOString());
  } else {
    console.log(new Date(date).toISOString());
  }
}

in Operator

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}

Summary

ConceptExample
Type Annotationlet name: string = 'Alice'
Type Inferencelet age = 25 (inferred as number)
Arrayslet nums: number[] = [1, 2, 3]
Tupleslet pair: [string, number] = ['a', 1]
Union Typeslet id: string | number
Literal Typestype Status = 'on' | 'off'
Type Aliasestype User = { name: string }
Type Assertionsvalue as string
Type Guardstypeof, instanceof, in
Next, we’ll explore functions and how TypeScript makes them safer and more expressive!