Basic Types in TypeScript
Master TypeScript's built-in types: string, number, boolean, null, undefined, any, unknown, never, and void — with practical usage guidance.
Primitive Types
TypeScript’s three core primitives map directly to JavaScript’s runtime primitives. Annotating them explicitly tells both TypeScript and other developers exactly what kind of value a variable holds, preventing whole categories of bugs like accidentally concatenating a number with a string.
const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
Numbers include integers, floats, hex, octal, and binary — TypeScript treats them all as the single number type, just like JavaScript:
const decimal: number = 42;
const float: number = 3.14;
const hex: number = 0xff; // 255
const binary: number = 0b1010; // 10
Strings can be literals or template literals. TypeScript understands template literal interpolation and types the result as string:
const first: string = "Hello";
const greeting: string = `${first}, world!`; // "Hello, world!"
null and undefined
JavaScript has always had both null and undefined, but without TypeScript they’re easy to forget about. TypeScript with strict: true makes you handle them explicitly, which eliminates an enormous class of runtime errors.
let notAssigned: undefined = undefined;
let empty: null = null;
With strict: true (which enables strictNullChecks), these are not assignable to other types — null can’t silently sneak into a variable you thought was a string:
let name: string = "Alice";
name = null; // Error: Type 'null' is not assignable to type 'string'
name = undefined; // Error
To allow null or undefined, declare it explicitly with a union type. This makes the possibility of absence visible and forces callers to handle it:
let name: string | null = "Alice";
name = null; // fine — we said this was allowed
// Caller must now check before using the result
function findUser(id: number): User | null {
// returns null if not found — callers can't ignore this
}
any
any is TypeScript’s escape hatch — it completely opts a value out of type checking. The compiler will accept any operation on an any value without complaint, which means bugs can slip through silently. Use it sparingly and only when you have a good reason.
let value: any = 42;
value = "hello"; // fine — any accepts any assignment
value = { x: 1 }; // fine
value.foo.bar.baz(); // no error from TypeScript — but will crash at runtime
Legitimate uses of any:
- Migrating a large JavaScript codebase to TypeScript incrementally — you can type files one at a time
- Interacting with untyped third-party code where
@typespackages don’t exist
unknown
unknown is the type-safe alternative to any. Like any, it can hold any value — but unlike any, you cannot use the value at all until you narrow its type with a type guard. This forces you to write the defensive code you should have written anyway, and TypeScript makes sure you don’t skip it.
function processInput(input: unknown): string {
// TypeScript won't let you use input directly:
// return input.toUpperCase(); // Error: Object is of type 'unknown'
// You must narrow the type first — then TypeScript knows it's safe:
if (typeof input === "string") {
return input.toUpperCase(); // fine — TypeScript knows it's a string here
}
return String(input); // fallback for other types
}
Use unknown for:
- Function parameters that accept arbitrary input (JSON parsing, event handlers)
- Values from external APIs or user input before validation
void
void is the return type for functions that produce side effects but return nothing meaningful. It signals to callers that they should not use the return value. Without it, TypeScript would infer undefined or any, which gives callers the wrong idea.
function logMessage(msg: string): void {
console.log(msg);
// no return statement needed — void means "nothing useful returned"
}
A variable of type void can only hold undefined. In practice you mostly see void as a function return type, not as a variable type:
let result: void = undefined;
never
never represents a value that can never exist. It seems abstract but it solves two very concrete problems: marking functions that genuinely can’t return, and letting TypeScript catch unhandled cases in exhaustive checks at compile time.
1. Functions that never return:
// TypeScript knows code after these calls is unreachable
function throwError(message: string): never {
throw new Error(message); // always throws, never returns
}
function infiniteLoop(): never {
while (true) {
// loops forever — never returns
}
}
2. Exhaustive checks in switch statements — this is where never pays off most:
type Shape = "circle" | "square" | "triangle";
function describeShape(shape: Shape): string {
switch (shape) {
case "circle":
return "round";
case "square":
return "four sides";
case "triangle":
return "three sides";
default:
// If we add "hexagon" to the Shape union and forget to handle it here,
// TypeScript will error: Type '"hexagon"' is not assignable to type 'never'
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${shape}`);
}
}
Array Types
Arrays are a fundamental collection type. TypeScript offers two equivalent syntaxes — pick one and be consistent across your codebase:
const numbers: number[] = [1, 2, 3]; // preferred for simple types
const names: Array<string> = ["Alice", "Bob"]; // useful for complex or generic types
Prefer T[] for simple cases, Array<T> when the type is complex or when you need to pass it as a type argument.
Tuple Types
Tuples are fixed-length arrays where each position has a known, potentially different type. They’re useful when you need to return multiple values from a function without creating an object — think of them as lightweight, unnamed structs.
const point: [number, number] = [10, 20];
const entry: [string, number] = ["age", 30];
// Destructuring works naturally — position determines the type
const [x, y] = point; // x: number, y: number
Named tuples (TypeScript 4.0+) make the meaning of each position explicit, improving readability significantly:
type Point = [x: number, y: number];
const origin: Point = [0, 0]; // the names "x" and "y" appear in editor hints
object and Object
Lowercase object represents any non-primitive value. It’s rarely useful on its own because it’s too broad — you can’t access any properties on it without narrowing. In practice, always prefer a specific interface or type literal.
function print(obj: object): void {
console.log(obj);
}
print({ name: "Alice" }); // fine
print(42); // Error — number is a primitive, not an object
Always use a specific shape instead of bare object:
// Better — you can actually use obj.name
function print(obj: { name: string }): void {
console.log(obj.name);
}
Symbol and BigInt
These two types cover specialized use cases. Symbols are unique identifiers used as object keys or as well-known constants. BigInt handles integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1) without precision loss.
const id: symbol = Symbol("id"); // unique, non-string key
const bigNumber: bigint = 9007199254740991n; // larger than Number.MAX_SAFE_INTEGER
bigint requires target to be ES2020 or higher in tsconfig.json.
Type Summary Table
A quick reference for choosing the right type when you’re not sure:
| Type | Holds | Use case |
|---|---|---|
string | Text | Names, messages, IDs |
number | Numbers | Counts, coordinates, prices |
boolean | true/false | Flags, toggles |
null | Intentional absence | Optional fields, API responses |
undefined | Uninitialized | Default for unset variables |
any | Anything, unchecked | Migration, untyped 3rd-party code |
unknown | Anything, checked | External input, JSON parsing |
void | undefined (from functions) | Side-effect-only functions |
never | Nothing | Throwing functions, exhaustive checks |
Practical Example
This function shows how these types work together at a system boundary — the kind of place where data arrives from outside and you don’t know its shape yet. Notice how unknown forces defensive handling and the return type makes the nullable result explicit.
// unknown input forces us to validate; number | null makes the nullable result explicit
function parseAge(raw: unknown): number | null {
// Handle the case where it's already a valid number
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
return raw;
}
// Handle string representations like "30" from form inputs
if (typeof raw === "string") {
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : null;
}
// Anything else (object, array, null, etc.) — not a valid age
return null;
}
parseAge(25); // 25
parseAge("30"); // 30
parseAge("abc"); // null
parseAge(null); // null
This is the kind of defensive code you write at system boundaries — where data comes in from outside and you can’t assume anything about its shape.