Episode 1 — Fundamentals / 1.25 — TypeScript Essentials

1.25.d — Basic Types

In one sentence: TypeScript provides a rich set of built-in typesstring, number, boolean, any, unknown, void, never, arrays, tuples, enums, and literal types — that let you describe the shape and constraints of your data with precision.

Navigation: ← 1.25 Overview · 1.25.e — Interfaces vs Type Aliases →


1. Type annotation syntax

TypeScript uses a colon after a variable name, parameter, or return position to annotate a type:

// Variable annotation
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;

// Function parameter and return type annotations
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function
const multiply = (a: number, b: number): number => a * b;

2. string — text values

let firstName: string = "Alice";
let greeting: string = `Hello, ${firstName}!`;  // Template literals work
let empty: string = "";

// All string methods work with full autocomplete:
firstName.toUpperCase();   // "ALICE"
firstName.includes("li");  // true
firstName.length;          // 5

3. number — all numeric values

TypeScript has one number type for integers, floats, hex, binary, and octal — no separate int or float:

let integer: number = 42;
let float: number = 3.14;
let negative: number = -10;
let hex: number = 0xff;         // 255
let binary: number = 0b1010;    // 10
let octal: number = 0o744;      // 484
let big: number = 1_000_000;    // Underscore separators allowed

// NaN and Infinity are also 'number'
let notANumber: number = NaN;
let infinite: number = Infinity;

For very large integers, use bigint:

let huge: bigint = 9007199254740991n;

4. boolean — true/false

let isLoggedIn: boolean = true;
let hasPermission: boolean = false;

// Boolean expressions produce boolean values
let isAdult: boolean = age >= 18;  // true

5. any — opt out of type checking

any tells TypeScript to skip all checking for this value. It accepts any type and can be used as any type:

let data: any = "hello";
data = 42;            // No error
data = { x: 1 };     // No error
data.foo.bar.baz();   // No error — even if this crashes at runtime!

// 'any' is contagious — it infects everything it touches
let result: any = getData();
let name = result.name;   // 'name' is also 'any' — no type safety

Avoid any whenever possible. It defeats the purpose of TypeScript. Use unknown instead (see next section).


6. unknown — type-safe alternative to any

unknown is the safe counterpart to any. You can assign anything to unknown, but you must narrow the type before using it:

let data: unknown = fetchSomething();

// Cannot use directly — errors:
// data.toUpperCase();    // Error: 'data' is of type 'unknown'
// data + 1;              // Error: 'data' is of type 'unknown'

// Must narrow first:
if (typeof data === "string") {
  data.toUpperCase();   // OK — TypeScript knows it's a string here
}

if (typeof data === "number") {
  data + 1;             // OK — TypeScript knows it's a number here
}

any vs unknown:

anyunknown
Assign anything to itYesYes
Use it without checkingYes (unsafe!)No — must narrow first
AutocompleteNoneFull, after narrowing
RecommendedMigration onlyYes, for truly unknown data

7. void — function returns nothing

void indicates a function does not return a meaningful value:

function logMessage(message: string): void {
  console.log(message);
  // No return statement, or: return; (no value)
}

// void is NOT the same as undefined
// void means "I don't care about the return value"

8. null and undefined as types

By default (with strictNullChecks), null and undefined are their own types and cannot be assigned to other types:

let maybeString: string | null = null;       // Explicitly nullable
let maybeName: string | undefined = undefined; // Explicitly optional

// Without union, these are errors:
// let name: string = null;       // Error with strictNullChecks
// let age: number = undefined;   // Error with strictNullChecks

9. never — function never returns

never represents values that never occur. Used for functions that always throw or have infinite loops:

// Function that always throws
function throwError(message: string): never {
  throw new Error(message);
}

// Function with infinite loop
function infiniteLoop(): never {
  while (true) {
    // ...
  }
}

// 'never' is also the type of impossible cases in exhaustive checks:
type Shape = "circle" | "square";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle": return Math.PI * 10 * 10;
    case "square": return 10 * 10;
    default:
      const _exhaustive: never = shape;  // Error if a case is missed
      return _exhaustive;
  }
}

10. Type inference

TypeScript can automatically infer types from assigned values — you do not always need explicit annotations:

// TypeScript infers these types automatically:
let name = "Alice";         // inferred as: string
let age = 30;               // inferred as: number
let isActive = true;        // inferred as: boolean
let items = [1, 2, 3];     // inferred as: number[]
let user = { name: "Alice", age: 30 };  // inferred as: { name: string; age: number }

// Function return types are also inferred:
function add(a: number, b: number) {  // return type inferred as: number
  return a + b;
}

When to annotate vs let inference work

SituationRecommendation
Variable initialized with a valueLet inference worklet x = 5
Function parametersAlways annotatefunction foo(x: number)
Function return typesAnnotate for public APIs, let inference work for internal helpers
Complex objectsAnnotate with interface/type for clarity
Empty arrays/objectsAnnotate — inference cannot guess: let items: string[] = []
Uninitialized variablesAnnotatelet name: string;
// Good — inference handles it
const message = "Hello";           // string
const numbers = [1, 2, 3];        // number[]

// Good — must annotate (empty or uninitialized)
let results: string[] = [];       // Cannot infer from empty array
let count: number;                // No initializer

// Good — annotate parameters, return type for clarity
function greet(name: string): string {
  return `Hello, ${name}!`;
}

11. Arrays

Two equivalent syntaxes for typed arrays:

// Syntax 1: type[]
let names: string[] = ["Alice", "Bob", "Charlie"];
let scores: number[] = [95, 87, 92];

// Syntax 2: Array<type> (generic syntax)
let names: Array<string> = ["Alice", "Bob", "Charlie"];
let scores: Array<number> = [95, 87, 92];

// Mixed arrays with union types
let mixed: (string | number)[] = ["Alice", 30, "Bob", 25];

// Array methods respect the type:
names.push("Dave");       // OK
names.push(42);           // Error: Argument of type 'number' is not assignable to parameter of type 'string'

// Readonly arrays
const frozenList: readonly string[] = ["a", "b", "c"];
// frozenList.push("d");  // Error: Property 'push' does not exist on type 'readonly string[]'

12. Tuples — fixed-length typed arrays

Tuples are arrays with a fixed number of elements where each position has a specific type:

// A tuple of [string, number]
let person: [string, number] = ["Alice", 30];

// Each position is typed:
person[0].toUpperCase();   // OK — position 0 is string
person[1].toFixed(2);      // OK — position 1 is number
// person[0].toFixed(2);   // Error — string has no toFixed

// Common use: function returning multiple values
function getCoordinates(): [number, number] {
  return [40.7128, -74.0060];
}

const [lat, lng] = getCoordinates();  // Destructuring works

// Named tuples (for documentation)
type UserTuple = [name: string, age: number, isActive: boolean];
const user: UserTuple = ["Alice", 30, true];

// Optional tuple elements
type Range = [start: number, end?: number];
const range1: Range = [0, 10];
const range2: Range = [0];     // 'end' is optional

// Rest elements in tuples
type StringAndNumbers = [string, ...number[]];
const data: StringAndNumbers = ["scores", 95, 87, 92, 100];

13. Enums — named constants

Enums define a set of named constants:

// Numeric enum (default — values auto-increment from 0)
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)

// String enum (recommended — more readable in logs/debugging)
enum Status {
  Pending = "PENDING",
  Active = "ACTIVE",
  Inactive = "INACTIVE",
}

let userStatus: Status = Status.Active;
console.log(userStatus);   // "ACTIVE"

// Using enums in conditions
if (userStatus === Status.Active) {
  console.log("User is active");
}

// Enum as function parameter
function handleStatus(status: Status): string {
  switch (status) {
    case Status.Pending: return "Waiting...";
    case Status.Active: return "Good to go!";
    case Status.Inactive: return "Account disabled.";
  }
}

// const enum — inlined at compile time (smaller output)
const enum Color {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

Note: Many TypeScript developers prefer union literal types over enums for simplicity:

// Union literal — often preferred over enum
type Direction = "up" | "down" | "left" | "right";
type Status = "pending" | "active" | "inactive";

14. Literal types — exact values as types

You can use specific values as types, not just broad categories:

// String literals
let direction: "up" | "down" | "left" | "right";
direction = "up";      // OK
direction = "diagonal"; // Error: Type '"diagonal"' is not assignable

// Number literals
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3;   // OK
let bad: DiceRoll = 7;    // Error

// Boolean literal
type True = true;

// Combining literals for precise types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function request(method: HttpMethod, url: string): Promise<{ status: StatusCode }> {
  // ...
}

// 'as const' makes all values literal types
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
} as const;
// typeof config = { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3 }

15. Type assertions — telling TypeScript what you know

Type assertions tell TypeScript "I know more about this type than you do":

// Syntax 1: 'as' keyword (preferred)
let value: unknown = "Hello, world!";
let length: number = (value as string).length;

// Syntax 2: angle bracket (does NOT work in .tsx files)
let length2: number = (<string>value).length;

// Common use case: DOM elements
const input = document.getElementById("username") as HTMLInputElement;
input.value = "Alice";   // Without assertion, TS only knows it's HTMLElement | null

// Non-null assertion operator (!)
const el = document.getElementById("app")!;  // Assert it's not null
// Use with caution — if it IS null, you'll get a runtime error

// 'as const' assertion — makes values readonly and literal
const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]  (not string[])

// 'satisfies' keyword (TypeScript 4.9+) — checks type WITHOUT widening
const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;
// palette.red is still number[] (not string | number[])
// palette.green is still string (not string | number[])

Warning: Type assertions override the compiler — use them sparingly. If you assert wrong, you get runtime bugs that TypeScript cannot catch.


16. Summary table — all basic types at a glance

TypeExampleDescription
string"hello"Text values
number42, 3.14All numeric values (int and float)
booleantrue, falseLogical values
anylet x: anyOpt out of type checking (avoid)
unknownlet x: unknownSafe alternative to any (must narrow)
void(): voidFunction returns nothing
nullnullIntentional absence of value
undefinedundefinedVariable declared but not assigned
never(): neverFunction never returns (throws/loops)
string[]["a", "b"]Array of strings
[string, number]["Alice", 30]Tuple — fixed-length typed array
enum Direction {}Direction.UpNamed constants
"up" | "down""up"Literal types — exact values
value as Typex as stringType assertion

Key takeaways

  1. TypeScript has primitive types: string, number, boolean, plus special types any, unknown, void, null, undefined, never.
  2. Type inference is powerful — you do not need to annotate everything; annotate parameters and ambiguous cases.
  3. unknown is safer than any — it forces you to narrow before using.
  4. Arrays use type[] or Array<type>; tuples use [type1, type2] for fixed-length.
  5. Enums define named constants; literal types ("up" | "down") are a lighter alternative.
  6. Type assertions (as Type) override the compiler — use sparingly and responsibly.

Explain-It Challenge

Explain without notes:

  1. What is the difference between any and unknown?
  2. When does TypeScript require you to write a type annotation (cannot infer)?
  3. What is a tuple, and how does it differ from a regular array?

Navigation: ← 1.25 Overview · 1.25.e — Interfaces vs Type Aliases →