Skip to main content

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 useState return: [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:

  1. Migration. You're converting a JavaScript codebase and need to type things incrementally. any is a temporary placeholder.

  2. Third-party code without types. A library has no @types/ package and you need to use it today. any gets you unstuck. (But open an issue on the library's repo asking for types.)

  3. 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 anyUseWhy
anyunknownForces you to check the type before using it
anyRecord<string, unknown>For objects with unknown shape
anyobjectWhen you need a non-primitive but don't care about shape
anyA proper interfaceBecause 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​

TypeExampleUse When
string"hello"Text data
number42, 3.14Numeric data
booleantrue, falseBinary states
nullnullExplicitly empty
undefinedundefinedNot yet assigned
symbolSymbol("id")Unique keys
bigint9007199254740991nVery large integers
string[]["a", "b"]Homogeneous arrays
[number, string][1, "a"]Fixed-shape arrays
readonly string[]["a", "b"]Immutable arrays
anyAnythingEscape hatch (avoid)
unknownAnythingUnknown data (safe)
neverNothingImpossible values
voidNo returnSide-effect functions
"up" | "down"Specific valuesConstrained choices

Try This: Type Foundations​

  1. Create an array of numbers and try to push a string. See the error.
  2. Create a tuple [string, number, boolean] and access each element. Notice how TypeScript knows the type at each index.
  3. Create a variable of type unknown. Try to call .toUpperCase() on it. Then narrow it with typeof and try again.
  4. Write a function that returns never (throw an error). Assign its return value to a variable and check the variable's type.
  5. 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.