The any type is the most dangerous four-letter word in programming. It's a seductive promise: "Don't worry about types. Just write code. I'll handle it." And for a while, it works. Your code compiles. Your tests pass. You ship. And then, at the worst possible moment, any cashes the check your code wrote — and the funds aren't there.
This chapter is about the alternative. The types that actually protect you. The types that make the compiler smart enough to catch bugs before you even know they exist. By the end, you'll understand every basic type in TypeScript, when to use each one, and — most importantly — why any is a trap you should almost never spring.
The Primitives: Your First Line of Defense
TypeScript has seven primitive types that map directly to JavaScript's runtime types. You already know the values. The types just make them explicit.
string
let name: string = "Alice";
let greeting = `Hello, ${name}`; // TypeScript infers string
string is the simplest type. It works exactly as you'd expect. The only surprise is that TypeScript distinguishes between string (the type) and String (the JavaScript wrapper object). Never use String as a type — it refers to the constructor, not the primitive.
number
let age: number = 30;
let price: number = 9.99;
let hex: number = 0xff; // 255
let binary: number = 0b1010; // 10
TypeScript doesn't distinguish between integers and floats. There's no int or float type — everything is number. This mirrors JavaScript's runtime behavior, where all numbers are 64-bit floating point.
boolean
let isActive: boolean = true;
let isComplete = false; // TypeScript infers boolean
Straightforward. true or false. TypeScript won't let you use a boolean where a number is expected, unlike JavaScript's loose equality.
null and undefined
let nothing: null = null;
let notDefined: undefined = undefined;
With strictNullChecks enabled (which it is, because you're using strict: true), null and undefined are distinct types. They don't belong to string, number, or any other type. This is the single biggest difference between JavaScript and TypeScript behavior:
let name: string = null;
// ~~~~
// Error: Type 'null' is not assignable to type 'string'.
let name: string | null = null; // OK — you explicitly allowed null
This forces you to handle the case where a value might not exist. It's annoying for about a week. Then it saves you from your first "Cannot read properties of null" in production, and you never go back.
symbol and bigint
let id: symbol = Symbol("id");
let huge: bigint = 9007199254740991n;
These are less common but fully supported. symbol is great for unique property keys. bigint handles integers beyond Number.MAX_SAFE_INTEGER.
Type Inference: Let the Compiler Do the Work
Here's something that surprises JavaScript developers new to TypeScript: you don't need to annotate everything. In fact, you SHOULDN'T annotate everything. TypeScript is smart enough to infer types from context:
let name = "Alice"; // TypeScript infers: string
let age = 30; // TypeScript infers: number
let isActive = true; // TypeScript infers: boolean
const names = ["Alice", "Bob", "Charlie"]; // TypeScript infers: string[]
const mixed = [1, "two", true]; // TypeScript infers: (string | number | boolean)[]
The rule of thumb: annotate function parameters and return types. Let inference handle the rest. Function signatures are your API contract — they should be explicit. Variable assignments are implementation details — inference is fine.
// DO annotate these
function calculateTotal(items: Item[]): number {
// DON'T annotate these (inference is fine)
const taxRate = 0.08;
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 + taxRate);
}
Arrays: Collections With Integrity
TypeScript arrays are typed collections. Once you declare an array of number, you can't push a string into it:
let scores: number[] = [95, 87, 92];
scores.push(88); // OK
scores.push("89"); // Error: Argument of type 'string' is not assignable
There's a second syntax using generics:
let scores: Array<number> = [95, 87, 92];
Both are equivalent. I prefer number[] because it's shorter and reads like "an array of numbers." Use whichever your team prefers, but be consistent.
Array Type Inference Nuances
TypeScript infers array types from the initial values:
const nums = [1, 2, 3]; // number[]
const strings = ["a", "b"]; // string[]
// Empty arrays need annotation — TypeScript can't infer from nothing
const items: string[] = [];
items.push("hello"); // OK
items.push(42); // Error
// Mixed arrays get union types
const mixed = [1, "two"]; // (string | number)[]
Readonly Arrays
Sometimes you want to ensure an array is never modified:
const colors: readonly string[] = ["red", "green", "blue"];
colors.push("yellow"); // Error: Property 'push' does not exist on 'readonly string[]'
colors[0] = "purple"; // Error: Index signature only permits reading
readonly arrays are great for constants, configuration, and any data that should be immutable. They also help the compiler optimize type checking.
Tuples: Arrays With a Fixed Shape
A tuple is an array with a known length and known type at each position:
let user: [number, string] = [1, "Alice"];
user[0]; // TypeScript knows this is number
user[1]; // TypeScript knows this is string
Tuples are perfect for:
- React's
useStatereturn:[state, setState] - Function return values with multiple pieces of data
- CSV row representations
- Coordinate pairs:
[number, number]
Named Tuples
TypeScript lets you label tuple positions for documentation:
type User = [id: number, name: string, email: string];
const user: User = [1, "Alice", "alice@example.com"];
The labels don't affect the type, but they show up in editor tooltips. Your future self will thank you.
Variadic Tuples
You can define tuples with a variable number of elements at the end:
type StringArray = [string, ...string[]];
// First element is string, rest are strings
type Mixed = [number, ...string[], boolean];
// Starts with number, has any number of strings, ends with boolean
This is powerful for typing functions like console.log that take a variable number of arguments.
The any Type: Understanding the Trap
any is TypeScript's escape hatch. When you annotate something as any, you're telling the compiler: "I don't want you to check this. Trust me."
let x: any = 5;
x = "hello"; // OK
x = true; // OK
x.fakeMethod(); // OK at compile time, crashes at runtime
x.nonexistent.property.deeply.nested; // OK at compile time, crashes at runtime
any is contagious. Once you use it, it spreads:
function getData(): any {
return fetch("/api/data").then(res => res.json());
}
const data = getData();
data.users; // Type: any — no checking
data.users[0].name; // Type: any — still no checking
data.users[0].name.toUpperCase(); // Type: any — hope it's a string!
Every property access on an any value returns any. Every function call with any arguments returns any. One any can silently un-type your entire codebase.
When any Is Actually OK
There are exactly three legitimate uses of any:
-
Migration. You're converting a JavaScript codebase and need to type things incrementally.
anyis a temporary placeholder. -
Third-party code without types. A library has no
@types/package and you need to use it today.anygets you unstuck. (But open an issue on the library's repo asking for types.) -
Truly dynamic code. You're writing something that genuinely can't be typed — like a JSON parser or a proxy that forwards arbitrary method calls. These cases are rare.
For everything else, there's a better option.
The any Alternatives
Instead of any | Use | Why |
|---|---|---|
any | unknown | Forces you to check the type before using it |
any | Record<string, unknown> | For objects with unknown shape |
any | object | When you need a non-primitive but don't care about shape |
any | A proper interface | Because you DO know the shape, you just haven't written it down |
unknown: The Safe Alternative
unknown is any's responsible sibling. It means "I don't know what this is" — but unlike any, it won't let you use it until you prove what it is:
let x: unknown = getData();
x.toUpperCase(); // Error: 'x' is of type 'unknown'
x.foo.bar; // Error: 'x' is of type 'unknown'
// You MUST narrow it first:
if (typeof x === "string") {
x.toUpperCase(); // OK — TypeScript now knows x is string
}
if (typeof x === "object" && x !== null && "name" in x) {
x.name; // OK — TypeScript now knows x has a name property
}
unknown is the type you should use when you genuinely don't know what something is. It forces you to handle every possibility, which is exactly what you should be doing with unknown data.
never: The Type That Shouldn't Exist
never is the most misunderstood type in TypeScript. It represents values that can never occur:
// A function that never returns (always throws)
function throwError(message: string): never {
throw new Error(message);
}
// A function that never returns (infinite loop)
function infiniteLoop(): never {
while (true) {}
}
// An impossible case in a switch
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return Math.PI * 5 * 5;
case "square":
return 10 * 10;
default:
// shape is type 'never' here — all cases are handled
const _exhaustive: never = shape;
return _exhaustive;
}
}
never is most useful for exhaustiveness checking. If you add a new variant to a union type, any switch statement that handles all cases will now have a type error in the default branch — because shape is no longer never, it's the new variant you forgot to handle.
void: When Nothing Is Something
void is the return type for functions that don't return a useful value:
function logMessage(message: string): void {
console.log(message);
// No return statement
}
In JavaScript, a function without a return statement returns undefined. But TypeScript uses void instead of undefined for this case. The distinction matters:
// void: "I don't care about the return value"
function callback1(): void {
console.log("done");
}
// undefined: "I explicitly return undefined"
function callback2(): undefined {
console.log("done");
return undefined;
}
// Practical difference:
const results: number[] = [];
[1, 2, 3].forEach(n => results.push(n)); // forEach expects (n: number) => void
// push returns number, but void accepts it — you're saying "I don't care about the return"
void is more flexible than undefined as a return type. A function typed as returning void CAN return a value — the caller just can't use it.
Literal Types: Values as Types
TypeScript lets you use specific values as types:
let direction: "up" | "down" | "left" | "right";
direction = "up"; // OK
direction = "north"; // Error: Type '"north"' is not assignable
let statusCode: 200 | 404 | 500;
statusCode = 200; // OK
statusCode = 201; // Error
let isReady: true;
isReady = true; // OK
isReady = false; // Error
Literal types are the foundation of discriminated unions (Chapter 7) and template literal types (Chapter 12). They let you encode business rules directly in the type system.
const vs let Inference
TypeScript infers literal types differently for const and let:
const name = "Alice"; // Type: "Alice" (literal — it can never change)
let name2 = "Alice"; // Type: string (widened — it could change)
const count = 5; // Type: 5 (literal)
let count2 = 5; // Type: number (widened)
This is why as const exists (which we'll cover in depth in Chapter 19). It tells TypeScript to infer the narrowest possible type:
const config = {
url: "https://api.example.com",
method: "GET",
} as const;
// Type: { readonly url: "https://api.example.com"; readonly method: "GET"; }
Type Assertions: When You Know Better
Sometimes you know more about a type than TypeScript does. Type assertions let you override the compiler:
const element = document.getElementById("app");
// Type: HTMLElement | null
// You KNOW the element exists:
const app = document.getElementById("app") as HTMLElement;
// Type: HTMLElement
// Alternative syntax (avoid in JSX/TSX files):
const app2 = <HTMLElement>document.getElementById("app");
Type assertions are NOT type casts. They don't convert anything at runtime. They're purely a compile-time directive: "I know this is an HTMLElement, trust me." If you're wrong, you get a runtime error.
Use assertions sparingly. Every assertion is a hole in the type system. The compiler can't verify your claim — it just believes you.
The Type Table: A Quick Reference
| Type | Example | Use When |
|---|---|---|
string | "hello" | Text data |
number | 42, 3.14 | Numeric data |
boolean | true, false | Binary states |
null | null | Explicitly empty |
undefined | undefined | Not yet assigned |
symbol | Symbol("id") | Unique keys |
bigint | 9007199254740991n | Very large integers |
string[] | ["a", "b"] | Homogeneous arrays |
[number, string] | [1, "a"] | Fixed-shape arrays |
readonly string[] | ["a", "b"] | Immutable arrays |
any | Anything | Escape hatch (avoid) |
unknown | Anything | Unknown data (safe) |
never | Nothing | Impossible values |
void | No return | Side-effect functions |
"up" | "down" | Specific values | Constrained choices |
Try This: Type Foundations
- Create an array of numbers and try to push a string. See the error.
- Create a tuple
[string, number, boolean]and access each element. Notice how TypeScript knows the type at each index. - Create a variable of type
unknown. Try to call.toUpperCase()on it. Then narrow it withtypeofand try again. - Write a function that returns
never(throw an error). Assign its return value to a variable and check the variable's type. - Create a literal type union:
type Status = "idle" | "loading" | "success" | "error". Try assigning an invalid status.
Time needed: 15 minutes.
What to notice: How TypeScript catches mistakes at each step. The unknown exercise is especially important — feel the difference between unknown (safe, requires checking) and any (unsafe, no checking).
The Revelation
You now know every basic type in TypeScript. But types in isolation are like words in isolation — they only become powerful when combined. The real magic of TypeScript's type system isn't in string or number. It's in how you COMBINE them: unions, intersections, generics, conditional types.
The next chapter starts that journey with functions — the most fundamental building block of any program. You'll learn how to type functions so precisely that they reject bad inputs at compile time, not at 2 AM when your error monitoring lights up.
In the next chapter: parameter types, return types, function overloads, and the critical difference between void and never in function return position.