Skip to content
C Codeloom
TypeScript

TypeScript Basic Types: number, string, boolean, and Beyond

A complete beginner's tour of TypeScript's primitive types, arrays, tuples, literal types, any, unknown, and the null and undefined story — with runnable examples.

·10 min read · By Yash Kesharwani
Beginner 11 min read

What you'll learn

  • Every primitive type TypeScript ships with
  • How to annotate variables explicitly and when to let inference do the work
  • Arrays and tuples and how they differ
  • Literal types and union types
  • What any and unknown actually do
  • The null / undefined story and strict null checks

Prerequisites

A type in TypeScript is a description of what kind of value something is. The compiler checks every operation in your program against the types of the values involved, and complains when they do not match. This post covers the small set of built-in types you will use in almost every TypeScript file you ever write.

Every example below runs as-is in the TypeScript Playground or by saving it to a .ts file and running tsc followed by node.

Annotating a variable

You declare a type with a colon after the variable name:

let age: number = 30;
let name: string = "Ada";
let isAdmin: boolean = false;

That colon-and-type is a type annotation. It tells the compiler what the variable is allowed to hold for the rest of its life.

If you try to break the contract, the compiler stops you:

let age: number = 30;
age = "thirty"; // Error: Type 'string' is not assignable to type 'number'.

You will see that error pattern hundreds of times. Read it slowly — Type X is not assignable to type Y always means “you tried to put X where the code expects Y.”

Inference: the types you do not have to write

TypeScript reads your code and figures most types out for itself. If you initialise a variable with a value, the type is inferred from that value:

let age = 30;          // inferred as number
let name = "Ada";      // inferred as string
let isAdmin = false;   // inferred as boolean

You do not have to write : number after age here — the compiler already knows. The community style is to let inference do the work for local variables and reserve explicit annotations for function parameters, return types, and exported values. Less noise, same safety.

const declarations are inferred even more tightly — we will come back to that under literal types.

The primitive types

TypeScript inherits its primitive types from JavaScript. The five you will use constantly:

let count: number = 42;
let pi: number = 3.14;
let big: bigint = 9007199254740993n;
let name: string = "Ada";
let isReady: boolean = true;
let nothing: null = null;
let missing: undefined = undefined;

A few notes:

  • number covers both integers and floats. There is no separate int or float.
  • bigint is for integers larger than Number.MAX_SAFE_INTEGER. You will rarely need it.
  • null and undefined each have their own type, and we will deal with them properly at the bottom of this post.

Arrays

There are two ways to write an array type. Both mean the same thing:

const nums: number[] = [1, 2, 3];
const words: Array<string> = ["one", "two", "three"];

The T[] form is more common in everyday code. The Array<T> form is sometimes clearer when T itself is long.

Once an array has a type, every operation respects it:

const nums: number[] = [1, 2, 3];
nums.push(4);          // ok
nums.push("five");     // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Inference works here too. const xs = [1, 2, 3] is inferred as number[] without an annotation.

Tuples

A tuple is a fixed-length array where each position has its own type. Useful when the shape carries meaning that an ordinary array would lose.

let point: [number, number] = [10, 20];
let entry: [string, number] = ["age", 30];

The compiler enforces both length and per-position types:

let point: [number, number] = [10, 20];
point = [10, 20, 30];        // Error: too many elements
point = ["x", 20];           // Error: string at position 0 not allowed

Tuples appear in real code mostly as return values — a React useState returns [T, (next: T) => void], for example.

Try it yourself. Declare a tuple person: [string, number, boolean] representing a name, age, and whether the person is an admin. Reassign one of the elements to a value of the wrong type and read the compiler error. Then read it a second time and notice exactly which position it is complaining about.

Literal types

A literal type is a type whose only allowed value is one specific value. This sounds useless until you see it.

let mode: "light" = "light";
mode = "dark";       // Error: Type '"dark"' is not assignable to type '"light"'.

On its own, that is silly. Combined with union types, it becomes one of the most useful patterns in the language:

let mode: "light" | "dark" = "light";
mode = "dark";       // ok
mode = "purple";     // Error

Now mode can hold exactly one of two strings — the compiler will refuse anything else. This is how TypeScript expresses what other languages use enums for.

You can union any types together, not just literals:

let id: number | string = 7;
id = "abc-123";              // ok
id = true;                   // Error

When you read a value from a union, the compiler only lets you use operations that work for every member. id.toUpperCase() would be rejected because number does not have it. We will cover narrowing — the way you tell the compiler which branch you are in — in a later post.

const and inferred literals

const interacts with literal types in a useful way. Because a const cannot be reassigned, the compiler narrows its inferred type to the literal value:

const direction = "up";      // inferred as the literal type "up"
let direction2 = "up";       // inferred as string

This matters when you pass the value somewhere that expects a specific literal:

function move(d: "up" | "down") {
  console.log(d);
}

const x = "up";
move(x);                     // ok — x is the literal "up"

let y = "up";
move(y);                     // Error — y is string, not "up" | "down"

A small thing with large consequences once you start writing functions that accept restricted sets of strings.

any — the escape hatch

any turns the type checker off for a value. Anything is assignable to any, and an any is assignable to anything else.

let x: any = 42;
x = "hello";        // ok
x = { foo: 1 };     // ok
x.bar.baz();        // ok — and a runtime crash waiting to happen

any exists for two reasons: gradual migration from JavaScript, and interop with libraries that have no type information. Both are legitimate. What you should not do is reach for any because a type is awkward. Every any is a hole the type checker stops looking through — large codebases tend to grow any like weeds if no one is paying attention.

A useful compiler flag is noImplicitAny, on by default in strict mode. It refuses to infer any silently, forcing you to either write a real type or opt in explicitly.

unknown — the safe alternative

unknown is what any should have been. It accepts any value, but you cannot use it until you have narrowed it to a specific type.

let value: unknown = JSON.parse('"hello"');

value.toUpperCase();           // Error: Object is of type 'unknown'.

if (typeof value === "string") {
  value.toUpperCase();         // ok — narrowed to string in this branch
}

The mental rule: use unknown when you genuinely do not know the type yet (parsed JSON, data from an external API, third-party callbacks). The compiler then forces you to check before you act, which is the entire point.

null, undefined, and strict null checks

Out of the box, TypeScript can be configured to treat null and undefined either as members of every type (loose) or as separate types you have to opt into (strict). In modern projects you should always use the strict mode — it is on by default in any project created with tsc --init.

With strict null checks on:

let name: string = "Ada";
name = null;          // Error: Type 'null' is not assignable to type 'string'.

If a value can genuinely be absent, you say so in the type:

let name: string | null = "Ada";
name = null;          // ok

And the compiler forces you to check before using it:

function shout(name: string | null) {
  return name.toUpperCase();           // Error: Object is possibly 'null'.
}

function shout2(name: string | null) {
  if (name === null) return "";
  return name.toUpperCase();           // ok — narrowed to string
}

This single feature eliminates a huge slice of real-world bugs — every time a value is unexpectedly missing and your code blows up reading a property off null. The compiler will simply not let you write that code anymore.

Try it yourself. Write a function length(value: string | null | undefined): number that returns 0 for null and undefined, and the actual length otherwise. Observe how the type narrows inside each branch when you hover the parameter.

A small worked example

Putting it all together — a sketch of a typed user record and a function that prints it.

type Role = "admin" | "editor" | "viewer";

interface User {
  id: number;
  name: string;
  email: string | null;
  role: Role;
}

function describe(user: User): string {
  const email = user.email ?? "no email on file";
  return `${user.name} (${user.role}): ${email}`;
}

const ada: User = {
  id: 1,
  name: "Ada Lovelace",
  email: "ada@example.com",
  role: "admin",
};

console.log(describe(ada));

Every feature in this post appears in those fifteen lines: a literal union, a typed object, a nullable field, and a function that narrows it. This is the everyday texture of real TypeScript.

Recap

You now know:

  • Variables are annotated with let name: Type = value, but inference handles most cases.
  • The primitive types are number, string, boolean, bigint, null, and undefined.
  • Arrays are written T[] or Array<T>. Tuples are fixed-length arrays with per-position types.
  • Literal types combined with unions express restricted sets of values, replacing what other languages use enums for.
  • any turns the checker off; unknown turns it on. Prefer unknown whenever you can.
  • Under strict null checks, null and undefined are separate types you must opt into and narrow before use.

Next steps

You have seen interface User { ... } and type Role = ... in the worked example. Both let you give a name to a shape and reuse it across the codebase. They are the next thing to learn properly — when to use each, how they differ, and how to compose them.

→ Next: TypeScript Interfaces and Type Aliases Explained

Questions or feedback? Email codeloomdevv@gmail.com.