Basic Types

The small set of built-in labels TypeScript uses to describe strings, numbers, booleans, arrays, and fixed-shape lists.

What is it?

Once you accept that TypeScript lets you describe what kind of value a variable holds, the next question is: what are the actual labels you use to describe it? TypeScript ships with a handful of basic ones that map directly onto the values JavaScript already has.

string describes text, number describes any numeric value (there's no separate "integer" or "float" — just one number type), and boolean describes true/false. Beyond single values, you'll often have a list of things — an array — written either as string[] (an array of strings) or Array<string> (the same thing, different notation).

Sometimes you know not just that something is a list, but exactly how many items it has and what type each specific position holds — for example, a pair that's always "a name, then an age." That fixed-length, fixed-order array is called a tuple, written like [string, number].

Finally, there's a type called any, which tells TypeScript "stop checking this — it could be anything." It's tempting to reach for when you're unsure what type something is, but doing so switches off the exact safety net TypeScript exists to provide for that value, letting mistakes slip through unnoticed. It's best treated as an escape hatch for rare cases, not a default.

Explain like I'm 10

Think of basic types like labels on storage bins — one bin is labeled "text only," another "numbers only," another "exactly one name and one age, in that order." The any bin has no label at all, so anything can go in — which is convenient, but it also means nothing stops you from mixing in something that doesn't belong.

Examples

The core primitive types

let username: string = "amara";
let age: number = 29;
let isActive: boolean = true;

// TypeScript catches mismatches immediately:
age = "twenty-nine";
// Error: Type 'string' is not assignable to type 'number'.

Each variable is annotated with the type of value it's allowed to hold. Once declared, TypeScript enforces that annotation on every later assignment.

Arrays, tuples, and any

let scores: number[] = [90, 85, 76];
let names: Array<string> = ["Sam", "Lee"];

// A tuple: always exactly [name, age], in that order
let profile: [string, number] = ["Priya", 34];

// any turns off checking for this value entirely — use sparingly
let response: any = fetchLegacyApi();
response.whatever.you.want(); // no error, even if this is wrong

Arrays hold any number of same-typed items. A tuple locks in both the number of items and each one's type by position. any opts a value out of type checking altogether, which is powerful but risky.

How it works

When you write let age: number = 29, the compiler records that age's type is number and checks every later use of age against that record — every reassignment, every place it's passed to a function, every comparison. This checking happens purely by reading your code (hence "static"); the compiler never actually runs age = "twenty-nine" to discover it's wrong, it infers the mismatch just from the code's structure.

any works by telling the compiler to skip building that record for a given value — so no checks are ever run against it, and any mismatch involving it slips through silently.

Why does it exist?

JavaScript already has these kinds of values at runtime (strings, numbers, booleans, arrays) — it just has no way to declare in advance which one a variable is supposed to hold. Basic types give you that vocabulary, so the compiler has something concrete to check your code against.

When to use it

Use explicit basic types whenever a value's type isn't obvious from context — function parameters, empty arrays you're about to fill, and tuples where position genuinely matters (like an [x, y] coordinate pair). For a value initialized immediately with an obvious literal, like let count = 0, TypeScript infers the type automatically and an annotation is often just extra noise.

When not to use it

Avoid reaching for any as a default whenever you're unsure of a type — it silently disables the exact checking you added TypeScript to get. Prefer figuring out the real type, or using unknown (a safer alternative that still forces you to check the value before using it) when you truly don't know what you're dealing with yet.

Common mistakes

  • Reaching for any whenever a type is inconvenient to figure out, which quietly turns off type checking for that value and everything derived from it.

  • Confusing a tuple with a regular array — [string, number] locks in exactly two items in that exact order, while (string | number)[] allows any number of items in any order.

  • Over-annotating obvious values, like writing let count: number = 0 when TypeScript would infer number automatically from the 0.

Practice exercises

  1. Easy:

    Declare three variables with explicit types: a string for a city name, a number for a population, and a boolean for whether it's a capital city.

  2. Medium:

    Create a tuple type representing an RGB color as [number, number, number], and write a variable of that type. Then try adding a fourth number and observe the error.

  3. Hard:

    Write a function describe(value: any) that logs value.length. Call it with a number and observe that TypeScript does not catch the runtime error. Then change the parameter type to unknown and see what TypeScript now requires you to do before accessing .length.

Interview questions

What's the difference between `string[]` and `[string, number]`?

string[] is an array of any length where every element is a string. [string, number] is a tuple: exactly two elements, the first a string and the second a number, checked by position.

Why is overusing `any` considered bad practice?

It disables type checking for that value entirely, letting mismatches and mistakes pass through silently — defeating the purpose of using TypeScript for that part of the code.

Does TypeScript have separate types for integers and floating-point numbers?

No, there is just one number type covering all numeric values, matching how JavaScript itself represents numbers.

What's the key difference between `any` and `unknown`?

Both can hold a value of any type, but any turns off type checking completely, while unknown still forces you to narrow the value (with a type guard, typeof check, or assertion) before you're allowed to use it in any specific way — it's a safe version of "could be anything."

Given `let x: unknown = getValue(); x.trim();`, does this compile?

No. Even though x might genuinely hold a string at runtime, TypeScript doesn't know that from its declared type unknown, so it refuses to let you call .trim() until you narrow x — for example with if (typeof x === "string") { x.trim(); }.

What type does TypeScript infer for `let count = 0;` when no annotation is written?

number. TypeScript looks at the initializer's value and infers the broader type it belongs to for a let (or var) variable, since the variable is expected to be reassigned later.

What type does TypeScript infer for `const count = 0;`, and how is that different from the `let` case?

For a const, TypeScript infers the narrower literal type 0, not the general number — because a const can never be reassigned, the compiler knows its value will always be exactly 0, so it keeps the more precise type.

Why does `let age: number = 29; age = "twenty-nine";` get rejected by TypeScript, even though the equivalent plain JavaScript runs fine?

JavaScript happily reassigns a variable to any type at runtime — it has no concept of a variable being locked to one type. TypeScript adds that concept: once age is annotated (or inferred) as number, every later assignment is checked against that annotation, and a string violates it, regardless of what JavaScript itself would allow.

Can you append a fourth item to a variable typed as `let profile: [string, number]` using `.push()`?

Surprisingly, yes in many TypeScript configurations — push on a tuple type is loosely checked and won't always error, even though reading a fourth element or reassigning the whole tuple with an extra item would be rejected. This is a well-known sharp edge in how tuples interact with mutating array methods.

At runtime, is there any difference between an array declared as `string[]` and one declared as `Array<string>`?

No — they're two notations for the exact same type, purely a stylistic choice. Both compile away to an ordinary JavaScript array; TypeScript treats them identically for checking purposes.

Why does `let profile: [string, number] = [34, "Priya"];` fail to compile?

A tuple checks not just the types present but their exact position — the first slot must be string and the second number. Here the values are in the wrong order, so even though both a string and a number are present, the assignment doesn't match the tuple's declared shape.

What's the practical downside of writing `let scores = [];` instead of `let scores: number[] = [];`?

Without an annotation or an initial value to infer from, TypeScript often infers the near-useless type any[], which means every element pushed into scores later goes unchecked. Declaring the intended element type up front keeps the array's contents checked from the start.

If you get back a JSON payload from an API and you don't yet know its exact shape, should you type it as `any` or `unknown`?

unknown — it's just as flexible for holding a value of unknown shape, but it forces you to check or narrow the value before doing anything with it, whereas any would let bad assumptions about the payload's shape pass through completely unchecked.

Can a `boolean`-typed variable hold values like `0`, `""`, or `null` the way a JavaScript `if` condition treats them as falsy?

No. The boolean type accepts only the two literal values true and false — TypeScript doesn't extend it to cover JavaScript's broader notion of "falsy" values, even though those values behave like false inside a runtime condition.

When would a tuple be a better fit than an interface for representing two related values, like a name and an age?

A tuple is a good fit when position alone conveys meaning and you don't need named fields — for example, a function that always returns [value, error]. An interface is better once the values benefit from named properties, or when there might be more than a couple of fields, since accessing by index is far less self-documenting than accessing by name.

Why is `any` sometimes called an "escape hatch," and why is it risky when it flows into other code?

It's meant for rare cases where you genuinely can't or don't want to express a type. The risk is that any is contagious: any value derived from an any — a property access, a function's return value — also becomes any by default, silently spreading the loss of checking outward through the rest of the code that touches it.

What is *type widening*, and where does it show up with basic types?

Type widening is TypeScript generalizing a specific value to a broader type when it infers a mutable variable's type — for example, inferring let x = 5 as number rather than the literal type 5, because x could later be reassigned to any other number.

Why doesn't declaring `let count: number = 0;` gain you anything over just writing `let count = 0;`?

TypeScript already infers number from the literal 0 on its own, so the explicit annotation states nothing the compiler didn't already know — it's redundant, adding visual noise without adding any new type safety.

Is `unknown` a subtype of every other type, the way `any` is often loosely described?

Not quite — any is compatible in both directions (it can be assigned to anything, and anything can be assigned to it, bypassing checks). unknown can accept any value being assigned into it, but a plain unknown cannot be assigned out to a more specific type without narrowing first — that asymmetry is exactly what makes it safer.

If a function parameter is typed `any`, does TypeScript check calls to methods on that parameter, like `param.whatever.you.want()`?

No — once a value's type is any, TypeScript stops checking property accesses, method calls, and arguments on it entirely, so a call like that will compile even if it would throw at runtime.