TypeScript Types Basics

Primitives, interfaces vs type aliases, union/intersection types, arrays, tuples, and optional/readonly properties.

Primitive types

TypeScript's basic types mirror JavaScript's runtime types, plus a few that exist only at compile time:

Typescript
let username: string = "Ada";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notSet: undefined = undefined;

let anything: any = "avoid this";      // opts out of type checking entirely
let unknownValue: unknown = fetchData(); // safer alternative to any — see below
let result: never;                      // a function that never returns (always throws/loops)
function logMessage(msg: string): void { // no meaningful return value
  console.log(msg);
}

In almost all everyday code, you don't need to write these annotations at all — TypeScript infers the type from the assigned value:

Typescript
let username = "Ada";   // inferred as string, no annotation needed
// username = 42;       // Error: Type 'number' is not assignable to type 'string'

Explicit annotations matter most on function parameters (which TypeScript cannot infer on their own) and in a few other specific spots.

any vs unknown

any completely disables type checking for that value — it's an escape hatch that should be rare and deliberate. unknown is the type-safe alternative: it accepts anything, but forces you to narrow the type before doing anything with it.

Typescript
function processAny(value: any) {
  value.toUpperCase();   // compiles fine — even though this crashes at runtime if value is a number
}

function processUnknown(value: unknown) {
  // value.toUpperCase();  // Error: Object is of type 'unknown'
  if (typeof value === "string") {
    value.toUpperCase();   // fine — TypeScript knows value is a string here
  }
}

Interfaces vs type aliases

Both interface and type describe the shape of an object, and for plain object shapes they're largely interchangeable:

Typescript
interface User {
  name: string;
  age: number;
}

type UserAlias = {
  name: string;
  age: number;
};

const u1: User = { name: "Ada", age: 30 };
const u2: UserAlias = { name: "Ada", age: 30 };

The practical differences:

interface type
Object shapes Yes Yes
Union types ("a" | "b") No Yes
Can be extended later (declaration merging) Yes — multiple interface User {} blocks merge automatically No — a duplicate type name is an error
Extending another shape interface B extends A type B = A & { ... } (intersection)

A common convention: use interface for object shapes that represent entities (especially in public library APIs, where declaration merging is a real feature), and type for unions, function signatures, tuples, or anything that isn't a plain object shape. In practice, most teams pick one convention and use it consistently.

Union and intersection types

A union (|) means "one of these types." An intersection (&) means "all of these types combined":

Typescript
type Status = "pending" | "active" | "closed";

function printStatus(status: Status) {
  console.log(status);
}

printStatus("active");   // fine
// printStatus("done");  // Error: not assignable to type 'Status'

type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;   // must have both name AND age

const p: Person = { name: "Ada", age: 30 };

Arrays and tuples

Typescript
const scores: number[] = [10, 20, 30];
const names: Array<string> = ["Ada", "Grace"];   // equivalent generic syntax

const point: [number, number] = [10, 20];        // tuple — fixed length, fixed types per position
// point[0] = "x";   // Error: Type 'string' is not assignable to type 'number'

const entry: [string, number] = ["age", 30];     // e.g. a labeled key/value pair

A tuple is just an array with a fixed, known length and a known type at each position — useful for things like [key, value] pairs or coordinate points, where plain number[] would lose that positional meaning.

Optional and readonly properties

Typescript
interface Product {
  readonly id: number;     // can only be set once, at creation
  name: string;
  discount?: number;       // optional — may be undefined
}

const product: Product = { id: 1, name: "Keyboard" };
console.log(product.discount);   // undefined — fine, it was declared optional

// product.id = 2;   // Error: Cannot assign to 'id' because it is a read-only property

readonly is a compile-time-only guarantee — it doesn't produce any runtime enforcement (unlike, say, a const binding), but it's exactly the kind of mistake TypeScript exists to catch before the code ships.

Common mistakes

  • Overusing any to silence a type error instead of fixing the actual type — this defeats the entire purpose of using TypeScript. Prefer unknown plus narrowing when a value's type is genuinely not known yet.
  • Assuming interface and type are always interchangeable — only type can express a union ("a" | "b"), and only interface supports declaration merging.
  • Forgetting that readonly and tuple length checks are compile-time only — they vanish in the compiled JavaScript output and provide zero runtime protection.

Interview questions

Q: When must you use type instead of interface? When you need a union type ("pending" | "active"), a tuple, a mapped type, or a type alias for a primitive or function signature — interface can only describe object shapes (or extend other object shapes), not these other constructs.

Q: Why is unknown considered safer than any? any disables type checking entirely for that value, so the compiler won't stop you from calling a method that doesn't exist on it, and the mistake only surfaces as a runtime crash. unknown accepts any value too, but forces you to narrow it (with typeof, instanceof, or a type guard) before you can do anything with it — so the compiler still protects you from operating on a type you haven't actually verified.