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:
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:
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.
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:
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":
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
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
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
anyto silence a type error instead of fixing the actual type — this defeats the entire purpose of using TypeScript. Preferunknownplus narrowing when a value's type is genuinely not known yet. - Assuming
interfaceandtypeare always interchangeable — onlytypecan express a union ("a" | "b"), and onlyinterfacesupports declaration merging. - Forgetting that
readonlyand 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.