Polyglot
Languages TypeScript operators
TypeScript § operators

Operators

TypeScript inherits JavaScript’s operator surface entirely and adds type operators — operators that work at the type level rather than the runtime. The principal additions: typeof (used at the type level), keyof, in, as (type assertion), satisfies, and ! (non-null assertion). The runtime operators include the conventional arithmetic and comparison forms, the strict equality (===), the nullish coalescing (??), the optional chaining (?.), and the spread (...). The combination — JavaScript’s operator surface plus TypeScript’s type-level operators — covers the routine expression and type-level computation surface.

Arithmetic

a + b                                             // addition (and string concatenation)
a - b                                             // subtraction
a * b                                             // multiplication
a / b                                             // division (always float; no integer division)
a % b                                             // remainder
a ** b                                            // exponentiation

-a                                                // unary negation
+a                                                // unary plus (converts to number)
++x                                               // prefix increment
x++                                               // postfix increment
--x                                               // prefix decrement
x--                                               // postfix decrement

JavaScript division is always floating-point (no separate integer division operator):

console.log(7 / 2);                              // 3.5
console.log(Math.floor(7 / 2));                  // 3
console.log(7 / 0);                              // Infinity (no exception)
console.log(0 / 0);                              // NaN
console.log(-7 % 2);                             // -1 (sign follows the dividend)

The + is overloaded between numeric addition and string concatenation:

console.log(1 + 2);                              // 3
console.log("a" + "b");                          // "ab"
console.log("1" + 2);                            // "12" (number coerced to string)
console.log(1 + "2");                            // "12"

The conventional defence against accidental string concatenation is template literals and explicit conversions:

const total = `Total: ${count + 1}`;             // explicit numeric, then format

For BigInt:

const big = 100n;
console.log(big + 50n);                          // 150n
console.log(big * 2n);                           // 200n
// console.log(big + 50);                         // ERROR: cannot mix BigInt and number

Comparison

a === b                                           // strict equality (no type coercion)
a !== b                                           // strict inequality
a < b
a > b
a <= b
a >= b

a == b                                            // loose equality (with coercion); avoid
a != b                                            // loose inequality; avoid

The conventional discipline is strict equality (===) — it does not perform type coercion:

console.log(0 == "");                            // true (loose)
console.log(0 === "");                           // false (strict)
console.log(null == undefined);                  // true (loose)
console.log(null === undefined);                 // false (strict)
console.log(NaN === NaN);                        // false (NaN is unique)

For NaN-aware comparison, Number.isNaN(x) is the conventional defence:

if (Number.isNaN(value)) { /* ... */ }

The conventional Project Style is to disable ==/!= via the eqeqeq rule of ESLint.

Logical operators

a && b                                            // logical AND (short-circuit)
a || b                                            // logical OR (short-circuit)
!a                                                // logical NOT
a ?? b                                            // nullish coalescing (since ES2020)

The && and || are short-circuit but return the operand value, not just true/false:

const a = 0 || "default";                        // "default"
const b = "value" && "next";                     // "next"
const c = null ?? "default";                     // "default"
const d = undefined ?? "default";                // "default"
const e = "" ?? "default";                       // ""  (nullish only checks null/undefined)

The principal differences:

  • || falls back if the left is falsy (0, "", false, null, undefined, NaN).
  • ?? falls back only if the left is null or undefined.

The ?? is conventionally preferred for “default value” patterns where 0 or "" are valid:

// Bad: treats 0 as "no value"
const port = config.port || 8080;

// Better: only uses default when port is null/undefined
const port = config.port ?? 8080;

Optional chaining ?.

The ?. admits “access this property if the receiver is not null/undefined”:

const name = user?.profile?.name;                 // undefined if any link is missing
const first = arr?.[0];                          // index access
const result = fn?.(arg);                         // function call

// Equivalent (more verbose):
const name = user && user.profile && user.profile.name;

The form short-circuits on null or undefined, returning undefined. Conventional for navigation through optional structures.

Bitwise operators

a & b                                             // AND
a | b                                             // OR
a ^ b                                             // XOR
~a                                                // NOT
a << b                                            // left shift
a >> b                                            // arithmetic right shift
a >>> b                                           // logical right shift (unique to JS)

The >>> is a JavaScript distinctive — it shifts in zeros from the left, producing an unsigned-style result:

console.log(-1 >> 0);                            // -1
console.log(-1 >>> 0);                           // 4294967295 (2^32 - 1)

JavaScript bitwise operators work on 32-bit integers; for BigInt, the same operators work on arbitrary-precision integers.

Assignment operators

x = y                                             // simple assignment
x += y                                            // x = x + y
x -= y                                            // x = x - y
x *= y
x /= y
x %= y
x **= y

x &= y
x |= y
x ^= y
x <<= y
x >>= y
x >>>= y

// Logical assignment (ES2021):
x &&= y                                           // x = x && y
x ||= y                                           // x = x || y
x ??= y                                           // x = x ?? y

The logical assignment operators are conventional for “set-if-unset” patterns:

config.timeout ??= 30;                           // assign only if null/undefined
items[key] ||= [];                               // assign empty array if not set

Spread ...

The spread operator admits unpacking arrays and objects:

// Array spread:
const a = [1, 2, 3];
const b = [...a, 4, 5];                          // [1, 2, 3, 4, 5]
const copy = [...a];                              // shallow copy

// Object spread:
const original = { a: 1, b: 2 };
const extended = { ...original, c: 3 };          // { a: 1, b: 2, c: 3 }

// Function arguments:
const args = [1, 2, 3];
console.log(Math.max(...args));                  // 3

// Destructuring rest:
const [first, ...rest] = [1, 2, 3, 4];
const { a, ...others } = { a: 1, b: 2, c: 3 };

The form is conventional for immutable updates and function composition.

Destructuring

The = admits destructuring patterns:

const [a, b, c] = [1, 2, 3];
const [a, , c] = [1, 2, 3];                      // skip middle
const [a, ...rest] = [1, 2, 3, 4];

const { name, age } = person;
const { name: n, age: a } = person;              // rename
const { name = "anon", age = 0 } = person;       // defaults
const { user: { profile: { name } } } = data;    // nested

// Function parameters:
function greet({ name, age }: { name: string; age: number }) {
    /* ... */
}

Treated as a substantial subset of TypeScript’s expression surface.

Type assertions as

The as operator tells the compiler “trust me, this value is of this type”:

const value: unknown = "hello";
const length = (value as string).length;

const input = document.getElementById("input") as HTMLInputElement;

// Const assertion:
const config = { host: "localhost", port: 8080 } as const;

Treated in Types.

Non-null assertion !

The postfix ! asserts that a value is not null or undefined:

function process(value: string | undefined) {
    const trimmed = value!.trim();               // assume value is defined
    return trimmed;
}

const element = document.getElementById("foo")!; // assume found

The conventional discipline avoids ! — narrowing or null checks are conventionally clearer:

if (value !== undefined) {
    return value.trim();                          // narrowed; no assertion needed
}

satisfies

Since TS 4.9, satisfies admits “value matches this type, preserve specific type”:

type Config = Record<string, string | number>;

const config = {
    host: "localhost",
    port: 8080,
} satisfies Config;

config.host.toUpperCase();                       // OK; host is "localhost", not string | number
config.port.toFixed(2);                          // OK; port is 8080, not string | number

Treated in Types.

Type-level operators

Operators that work at the type level (in type expressions, not runtime):

typeof

const config = { host: "localhost", port: 8080 };
type Config = typeof config;                     // { host: string; port: number }

keyof

interface Person {
    name: string;
    age: number;
}

type Keys = keyof Person;                        // "name" | "age"

Indexed access

type Name = Person["name"];                      // string
type Values = Person[keyof Person];              // string | number

extends (in conditional types)

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;                      // true
type B = IsString<42>;                           // false

Treated in Advanced types.

Operator precedence

The principal precedence levels (high to low):

OperatorPrecedence
(), [], .20 (highest)
**14
*, /, %13
+, -12
<<, >>, >>>11
<, <=, >, >=, in, instanceof10
==, !=, ===, !==9
&8
^7
|6
&&5
||, ??4
?:3
=, +=, etc.2
,1 (lowest)

The conventional discipline uses parentheses for clarity in non-trivial expressions.

The in operator

in checks whether a key exists in an object:

const obj = { a: 1, b: 2 };

if ("a" in obj) {
    console.log(obj.a);                           // OK
}

// Type narrowing:
type A = { kind: "a"; value: number };
type B = { kind: "b"; data: string };
type AB = A | B;

function process(x: AB) {
    if ("value" in x) {
        x.value;                                 // OK; narrowed to A
    } else {
        x.data;                                  // OK; narrowed to B
    }
}

The mechanism admits substantial discrimination on object shapes.

The instanceof operator

instanceof checks the prototype chain:

class Animal {}
class Dog extends Animal {}

const d = new Dog();
console.log(d instanceof Dog);                   // true
console.log(d instanceof Animal);                // true

// Narrowing:
function process(x: Animal | string) {
    if (x instanceof Animal) {
        x.someMethod();                           // narrowed to Animal
    } else {
        x.toUpperCase();                          // narrowed to string
    }
}

The void operator

The void expression evaluates the expression and produces undefined:

console.log(void 0);                             // undefined

// Conventional in URL "javascript:" pseudo-protocols:
// <a href="javascript:void(0)" onclick="...">

// Or for fire-and-forget async:
void asyncTask();                                 // makes intent explicit

The conventional uses are rare in modern code.

The delete operator

delete removes a property:

const obj = { a: 1, b: 2 };
delete obj.a;
console.log(obj);                                // { b: 2 }

The conventional discipline avoids delete — it produces “holes” in objects and disables hidden-class optimisations. The conventional defence is to set the property to undefined or rebuild the object:

const { a, ...rest } = obj;                      // produces a copy without "a"

Common patterns

Default value with ??

const port = config.port ?? 8080;
const name = user.name ?? "anonymous";

Conditional access with ?.

const country = user?.address?.country;
const first = items?.[0];
const result = callback?.(arg);

Logical-assignment for setup

function getOrCreate(key: string) {
    cache[key] ??= computeExpensive(key);
    return cache[key];
}

Spread for immutable update

function updateUser(user: User, changes: Partial<User>): User {
    return { ...user, ...changes };
}

function addItem(items: Item[], newItem: Item): Item[] {
    return [...items, newItem];
}

Destructuring in parameters

function format({ name, age }: { name: string; age: number }): string {
    return `${name} (${age})`;
}

function fetchData({ url, method = "GET", headers = {} }: RequestConfig) {
    /* ... */
}

Type narrowing with in

function area(shape: Circle | Square): number {
    if ("radius" in shape) {
        return Math.PI * shape.radius ** 2;
    }
    return shape.side ** 2;
}

Optional callback

function process(items: Item[], onProgress?: (n: number) => void) {
    items.forEach((item, i) => {
        doWork(item);
        onProgress?.(i + 1);                     // call only if defined
    });
}

A note on the conventional discipline

The contemporary TypeScript operator advice:

  • Use === and !== — never == or !=.
  • Use ?? over || for default values.
  • Use ?. for optional access.
  • Use spread (...) for immutable updates.
  • Use destructuring in parameters and assignments.
  • Use as const for literal-type precision.
  • Use satisfies for type-checked-but-precise values.
  • Avoid ! — prefer narrowing or explicit checks.
  • Avoid delete — prefer immutable updates.
  • Use parentheses for clarity in mixed-precedence expressions.

The combination — JavaScript’s operator surface, the type-level operators (typeof, keyof, in), the modern conveniences (??, ?., ..., satisfies) — is the substance of TypeScript’s expression surface. The discipline produces clear, type-safe, immutable code.