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 isnullorundefined.
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):
| Operator | Precedence |
|---|---|
(), [], . | 20 (highest) |
** | 14 |
*, /, % | 13 |
+, - | 12 |
<<, >>, >>> | 11 |
<, <=, >, >=, in, instanceof | 10 |
==, !=, ===, !== | 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 constfor literal-type precision. - Use
satisfiesfor 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.