Skip to main content
TypeScript beginner Lesson 3 of 21

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 @types packages 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:

TypeHoldsUse case
stringTextNames, messages, IDs
numberNumbersCounts, coordinates, prices
booleantrue/falseFlags, toggles
nullIntentional absenceOptional fields, API responses
undefinedUninitializedDefault for unset variables
anyAnything, uncheckedMigration, untyped 3rd-party code
unknownAnything, checkedExternal input, JSON parsing
voidundefined (from functions)Side-effect-only functions
neverNothingThrowing 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.

Frequently Asked Questions

What is the difference between any and unknown?
Both can hold any value, but unknown is safer. With any you can do anything to the value without checks. With unknown you must narrow the type first before using it, which forces you to handle edge cases.
When should I use never?
Use never for functions that never return (they throw or loop forever) and as the bottom of exhaustive checks. TypeScript infers never automatically in many cases.
Should I ever use any?
Avoid it when possible. any completely disables type checking for that value. Prefer unknown for values whose type you genuinely don't know, and use type guards to narrow from there.