Beginner

Variables and Types in TypeScript

Learn variable declarations, primitive types, type inference, unions, and type conversions in TypeScript with Docker-ready examples

TypeScript is a typed superset of JavaScript, and its type system is where the language earns its keep. While JavaScript will happily let you assign any value to any variable and worry about it later, TypeScript checks your work at compile time — catching whole classes of bugs before the code ever runs.

Variables in TypeScript are declared with the same let, const, and (legacy) var keywords from JavaScript, but each binding can carry an optional type annotation. The compiler also performs aggressive type inference: if you write let count = 0, TypeScript already knows count is a number. You only need to spell out types when inference isn’t enough or when documenting an interface.

The type system is structural (a value matches a type if it has the right shape, regardless of how it was declared) and strong (no implicit coercions between unrelated types). It also models JavaScript’s quirks faithfully: null and undefined are distinct types, and with strict mode enabled, the compiler forces you to handle them explicitly.

This tutorial covers variable declarations, the primitive types, literal and union types, immutability with const and readonly, and how type conversions work in practice.

Variable Declarations and Primitive Types

Create a file named variables.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// --- let vs const ---
// `let` declares a mutable binding; `const` declares an immutable one.
let counter: number = 0;
counter = counter + 1;

const greeting: string = "Hello, TypeScript!";
// greeting = "nope"; // would error: Cannot assign to 'greeting' because it is a constant.

// --- Type inference ---
// TypeScript infers the type from the initializer; the annotation is optional.
let pi = 3.14159;          // inferred as number
let language = "TypeScript"; // inferred as string
let active = true;          // inferred as boolean

// --- Primitive types ---
const wholeNumber: number = 42;
const decimalNumber: number = 3.14;
const big: bigint = 9007199254740993n; // arbitrary-precision integer
const name: string = "Ada";
const isReady: boolean = true;
const nothing: null = null;
const notSet: undefined = undefined;
const tag: symbol = Symbol("id");

// --- Template literals (strings with embedded expressions) ---
const summary: string = `${name} is ${wholeNumber} (ready: ${isReady})`;

// --- Arrays and tuples ---
const scores: number[] = [95, 87, 76];
const pair: [string, number] = ["age", 30]; // fixed-length tuple

// Output everything
console.log(`counter   = ${counter}`);
console.log(`greeting  = ${greeting}`);
console.log(`pi        = ${pi}`);
console.log(`language  = ${language}`);
console.log(`active    = ${active}`);
console.log(`big       = ${big}`);
console.log(`summary   = ${summary}`);
console.log(`scores    = [${scores.join(", ")}]`);
console.log(`pair      = [${pair[0]}, ${pair[1]}]`);
console.log(`nothing   = ${nothing}`);
console.log(`notSet    = ${notSet}`);
console.log(`tag type  = ${typeof tag}`);

A few things worth highlighting:

  • number covers everything numeric, integer or float — there is no separate int type. Use bigint (with the n suffix literal) when you need integers larger than 2^53 - 1.
  • const blocks reassignment, not mutation. const xs = [1, 2]; xs.push(3) is legal because the binding still points to the same array.
  • Tuples are arrays whose length and per-position types are tracked by the compiler.

Type Inference, Unions, and Literal Types

TypeScript’s most distinctive feature is the way the type system narrows as you check values. A union type like string | number lets a variable hold either, and conditional checks teach the compiler which one is present at each point.

Create a file named inference.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// `id` can be a string OR a number — a union type.
let id: string | number = "abc-123";
console.log(`id (string form) = ${id}, length = ${id.length}`);

id = 42; // legal: number is part of the union
console.log(`id (number form) = ${id}, doubled = ${id * 2}`);

// --- Type narrowing with typeof ---
function describe(value: string | number): string {
    if (typeof value === "string") {
        // Inside this branch, TypeScript knows `value` is a string.
        return `string of length ${value.length}`;
    }
    // Outside the branch, only `number` remains.
    return `number with value ${value.toFixed(2)}`;
}

console.log(describe("hello"));
console.log(describe(7.5));

// --- Literal types ---
// A variable can be typed to a specific value, not just a category.
let direction: "north" | "south" | "east" | "west" = "north";
direction = "east";
// direction = "up"; // would error: not assignable to that union of literals.
console.log(`direction = ${direction}`);

// --- `const` infers literal types automatically ---
const mode = "production"; // inferred as the literal type "production", not string
console.log(`mode = ${mode}`);

// --- Type aliases keep things readable ---
type Status = "pending" | "active" | "done";
const current: Status = "active";
console.log(`status = ${current}`);

Literal types and unions together replace the role enums play in many other languages — they describe a fixed set of valid values, and the compiler enforces it.

Type Conversions and Strict Null Handling

JavaScript is famous for surprising implicit conversions. TypeScript doesn’t change the runtime behavior, but it does require explicit conversions where the types don’t line up. With strict mode, it also forces you to acknowledge null and undefined.

Create a file named conversions.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// --- Explicit conversions between primitives ---
const numericString: string = "123";
const parsed: number = Number(numericString);     // 123
const parsedInt: number = parseInt("42px", 10);   // 42 (stops at non-digit)
const parsedFloat: number = parseFloat("3.14abc"); // 3.14

console.log(`Number("123")        = ${parsed}`);
console.log(`parseInt("42px", 10) = ${parsedInt}`);
console.log(`parseFloat("3.14abc")= ${parsedFloat}`);

// --- Number to string ---
const n: number = 255;
const asDecimal: string = n.toString();      // "255"
const asHex: string = n.toString(16);        // "ff"
const asFixed: string = n.toFixed(2);        // "255.00"
console.log(`${n} -> "${asDecimal}", hex "${asHex}", fixed "${asFixed}"`);

// --- Boolean conversion ---
console.log(`Boolean("")     = ${Boolean("")}`);     // false
console.log(`Boolean("text") = ${Boolean("text")}`); // true
console.log(`Boolean(0)      = ${Boolean(0)}`);      // false
console.log(`Boolean(1)      = ${Boolean(1)}`);      // true

// --- Optional values: undefined and null ---
// Under strict mode, this property might be missing.
interface User {
    name: string;
    nickname?: string; // optional
}

const u1: User = { name: "Grace" };
const u2: User = { name: "Ada", nickname: "Countess" };

function displayName(user: User): string {
    // The compiler forces us to handle the possibly-undefined case.
    return user.nickname ?? user.name; // ?? falls back when null/undefined
}

console.log(`u1 -> ${displayName(u1)}`);
console.log(`u2 -> ${displayName(u2)}`);

// --- Type assertions: telling the compiler what you know ---
// Use sparingly — assertions bypass checks rather than prove safety.
const raw: unknown = "TypeScript";
const asString = raw as string;
console.log(`asserted length = ${asString.length}`);

Three TypeScript-specific touches in this example are worth flagging:

  1. ?: on a property marks it optional. The field’s type effectively becomes T | undefined.
  2. ?? (nullish coalescing) returns the right-hand side only when the left side is null or undefined, unlike || which also triggers on empty strings and zero.
  3. as T (type assertion) tells the compiler to trust you — useful at boundaries where you know more than the type system, but it does not perform a runtime check.

Running with Docker

1
2
3
4
5
6
7
# Pull the official Node.js image
docker pull node:22-alpine

# Run each example with ts-node (downloaded on first use via npx)
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node variables.ts'
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node inference.ts'
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node conversions.ts'

The npx -y ts-node <file> command compiles the TypeScript and executes it in one step, with no tsconfig.json required for these standalone scripts.

Expected Output

Running variables.ts:

counter   = 1
greeting  = Hello, TypeScript!
pi        = 3.14159
language  = TypeScript
active    = true
big       = 9007199254740993
summary   = Ada is 42 (ready: true)
scores    = [95, 87, 76]
pair      = [age, 30]
nothing   = null
notSet    = undefined
tag type  = symbol

Running inference.ts:

id (string form) = abc-123, length = 7
id (number form) = 42, doubled = 84
string of length 5
number with value 7.50
direction = east
mode = production
status = active

Running conversions.ts:

Number("123")        = 123
parseInt("42px", 10) = 42
parseFloat("3.14abc")= 3.14
255 -> "255", hex "ff", fixed "255.00"
Boolean("")     = false
Boolean("text") = true
Boolean(0)      = false
Boolean(1)      = true
u1 -> Grace
u2 -> Countess
asserted length = 10

Key Concepts

  • let is mutable, const is not — and const doesn’t make objects deeply immutable, only the binding itself.
  • Type inference is aggressive — write annotations only when they document an interface or when inference doesn’t know enough.
  • number is the only general numeric type; reach for bigint (with the n literal suffix) when integers may exceed 2^53 - 1.
  • Union types (A | B) let a value be one of several types; the compiler narrows the type as you check it with typeof or equality.
  • Literal types ("north" | "south") constrain a value to a fixed set, replacing many traditional enum use cases.
  • null and undefined are distinct types; under strict mode the compiler forces you to handle the possibility that a value is missing.
  • Conversions are explicit — use Number(...), String(...), Boolean(...), parseInt, or .toString(radix) rather than relying on implicit coercion.
  • as T is a type assertion, not a cast — it changes the compiler’s view but does not check or transform the value at runtime.

Running Today

All examples can be run using Docker:

docker pull node:22-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining