Output & Running
Hello, World
<?php
echo "Hello, World!\n"; console.log("Hello, World!"); There is no
<?php tag and no $ on variables. Output goes through console.log, which adds a newline for you, so you do not append \n the way you would with echo.Transpiling & Type Erasure
<?php
// PHP is interpreted; its type declarations are checked at RUNTIME:
function double(int $value): int {
return $value * 2;
}
echo double(21), "\n"; // 42 // TypeScript is transpiled to JavaScript; the types are ERASED:
function double(value: number): number {
return value * 2;
}
console.log(double(21)); // 42
// At runtime this is just: function double(value) { return value * 2; } This is the deepest difference. PHP's type declarations are enforced while the program runs — pass the wrong type and you get a
TypeError. TypeScript's annotations are checked only by the compiler and then stripped; the running JavaScript has no idea they existed and performs no type checks.Running TypeScript
<?php
// Run a PHP script directly:
// php script.php
$name = "World";
echo "Hello, {$name}!\n"; // Type-check + run in one step with tsx (or Deno/Bun):
// tsx script.ts
// Or compile to .js with the TypeScript compiler:
// tsc script.ts && node script.js
const name: string = "World";
console.log(`Hello, ${name}!`); Unlike PHP's direct interpretation, TypeScript needs a build or a runner that transpiles on the fly. Tools like
tsx, Deno, and Bun execute a .ts file in one command, while tsc produces plain JavaScript for Node or the browser.Variables & Annotations
let, const & Annotations
<?php
$count = 42;
$name = "Alice";
define("MAX", 100); // a true constant
const RATE = 0.05; // also a constant
echo "{$count} {$name} {$RATE}\n"; let count: number = 42; // mutable, type-annotated
const name: string = "Alice"; // cannot be reassigned
const max = 100; // type inferred as number
console.log(`${count} ${name} ${max}`); Variables have no
$ sigil and are declared with let (mutable) or const (single assignment). A type annotation follows the name after a colon — count: number — the reverse of PHP's leading int $count form. Prefer const by default, as the TypeScript community does.Union & Nullable Types
<?php
// PHP 8 has union types and nullable types:
function describe(int|string $value): string {
return "got: " . $value;
}
function findName(?string $input): string {
return $input ?? "anonymous";
}
echo describe(42), "\n";
echo findName(null), "\n"; // anonymous function describe(value: number | string): string {
return "got: " + value;
}
function findName(input: string | null): string {
return input ?? "anonymous";
}
console.log(describe(42));
console.log(findName(null)); // anonymous PHP 8's union types (
int|string) and TypeScript's (number | string) are the same idea with near-identical syntax. PHP's nullable shorthand ?string becomes the explicit string | null in TypeScript — and note TypeScript distinguishes null from undefined, where PHP has only null.One number Type
<?php
$integer = 7;
$float = 3.5;
echo intdiv(7, 2), "\n"; // 3
echo 7 / 2, "\n"; // 3.5
echo gettype($integer), "\n"; // integer
echo gettype($float), "\n"; // double const integer = 7;
const float = 3.5;
console.log(Math.trunc(7 / 2)); // 3
console.log(7 / 2); // 3.5
console.log(typeof integer); // number
console.log(typeof float); // number JavaScript — and therefore TypeScript — has a single
number type (a 64-bit float); there is no separate integer type, so typeof 7 and typeof 3.5 are both "number". Division never truncates, so use Math.trunc for integer division. For very large integers TypeScript offers a separate bigint type.Type Inference
Inferred Types
<?php
// PHP variables are untyped; only declarations carry types:
$numbers = [1, 2, 3];
$total = array_sum($numbers); // PHP infers nothing — $total is just mixed
echo $total, "\n"; // 6 // TypeScript infers a precise type from the initializer:
const numbers = [1, 2, 3]; // inferred: number[]
const total = numbers.reduce((sum, n) => sum + n, 0); // inferred: number
console.log(total); // 6
// const broken: string = total; // compile error — number is not string TypeScript's inference means you rarely write annotations for local variables: the compiler reads
[1, 2, 3] and knows it is number[]. This gives you static checking for free — assigning that number to a string would be caught at compile time, something PHP cannot do for an untyped variable.as const & Literal Inference
<?php
// PHP constants are values; there is no literal-type concept:
const DIRECTION = "north";
echo DIRECTION, "\n"; // north
$status = "active"; // just a string
echo $status, "\n"; let direction = "north"; // inferred as string (widened)
const fixed = "north"; // inferred as the literal type "north"
const config = { mode: "dark" } as const; // deeply readonly literals
console.log(direction, fixed, config.mode); TypeScript infers literal types: a
const string is typed as its exact value ("north"), not the broad string. The as const assertion freezes an entire object into readonly literal types — a precision tool with no PHP analogue, used heavily to model fixed sets of values.Strings
Template Literals
<?php
$name = "Alice";
$age = 30;
echo "{$name} is {$age}\n";
echo $name . " is " . $age . "\n"; const name = "Alice";
const age = 30;
console.log(`${name} is ${age}`);
console.log(name + " is " + age); TypeScript template literals use backticks with
\${...} holes, the counterpart to PHP's "{$name}" interpolation. Concatenation with + auto-converts the number to a string here (like PHP's .), but beware: + is also numeric addition, so the operand types matter — see the gotchas.Common Methods
<?php
$text = " Hello, World ";
echo strtoupper(trim($text)), "\n"; // HELLO, WORLD
echo str_replace("World", "TS", "Hello World"), "\n";
echo str_contains("Hello", "ell") ? "yes\n" : "no\n"; const text = " Hello, World ";
console.log(text.trim().toUpperCase()); // HELLO, WORLD
console.log("Hello World".replace("World", "TS"));
console.log("Hello".includes("ell") ? "yes" : "no"); Strings are objects with chainable methods (
text.trim().toUpperCase()) rather than the free functions strtoupper(trim($text)). Substring testing is includes, and the method names are camelCase — the JavaScript convention TypeScript inherits.Split & Join
<?php
$csv = "a,b,c";
$parts = explode(",", $csv);
print_r($parts);
echo implode("-", $parts), "\n"; // a-b-c const csv = "a,b,c";
const parts: string[] = csv.split(",");
console.log(parts); // [ 'a', 'b', 'c' ]
console.log(parts.join("-")); // a-b-c The pair is
split / join rather than explode / implode, and both are methods — join is called on the array with the separator as its argument, the opposite arrangement from implode($glue, $array).Arrays, Tuples & Objects
One Array vs Several Types
<?php
// PHP's single array is both list and map:
$list = [1, 2, 3];
$map = ["name" => "Alice", "age" => 30];
echo $list[0], " ", $map["name"], "\n"; // TypeScript separates these into distinct, typed structures:
const list: number[] = [1, 2, 3];
const record: { name: string; age: number } = { name: "Alice", age: 30 };
const map = new Map<string, number>([["a", 1]]);
console.log(list[0], record.name, map.get("a")); PHP's one ordered-map array splits in TypeScript into typed arrays (
number[]), object types for fixed-shape records, and a real Map for dynamic key-value collections. Each carries element types the compiler checks, so list.push("x") would be a compile error.Array Operations
<?php
$numbers = [3, 1, 2];
$numbers[] = 4; // append
sort($numbers);
echo count($numbers), "\n"; // 4
echo in_array(2, $numbers) ? "yes\n" : "no\n"; const numbers: number[] = [3, 1, 2];
numbers.push(4);
numbers.sort((a, b) => a - b); // numeric sort needs a comparator!
console.log(numbers.length); // 4
console.log(numbers.includes(2)); // true Arrays expose methods:
push, sort, length, and includes replace array_push, sort, count, and in_array. A sharp trap — sort() with no comparator sorts lexicographically (so [10, 2] becomes [10, 2] wrongly), so always pass (a, b) => a - b for numbers.Map, Filter & Reduce
<?php
$numbers = [1, 2, 3, 4, 5];
$doubled = array_map(fn($n) => $n * 2, $numbers);
$evens = array_filter($numbers, fn($n) => $n % 2 === 0);
$total = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);
print_r($doubled);
print_r(array_values($evens));
echo $total, "\n"; // 15 const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2);
const evens = numbers.filter((n) => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log(doubled); // [ 2, 4, 6, 8, 10 ]
console.log(evens); // [ 2, 4 ]
console.log(total); // 15 The trio
map / filter / reduce are methods that chain in execution order, far cleaner than PHP's mix of array_map (callback first) and array_filter (array first). They always return a fresh, contiguously indexed array — no array_values reindexing needed.Tuples & Destructuring
<?php
// PHP destructures arrays with [] / list():
[$x, $y] = [10, 20];
echo "{$x},{$y}\n"; // 10,20
["name" => $name] = ["name" => "Alice"];
echo $name, "\n"; // Alice const point: [number, number] = [10, 20]; // a fixed-length tuple type
const [x, y] = point; // array destructuring
console.log(`${x},${y}`); // 10,20
const { name } = { name: "Alice", age: 30 };// object destructuring
console.log(name); // Alice TypeScript adds a tuple type
[number, number] — a fixed-length, per-position-typed array with no PHP equivalent. Destructuring covers both PHP forms: [x, y] for arrays and { name } for objects, the latter pulling a property by name like PHP's keyed list destructuring.Interfaces & Structural Typing
Interfaces as Shapes
<?php
// A PHP interface is a contract a class must explicitly implement:
interface HasArea {
public function area(): float;
}
class Square implements HasArea {
public function __construct(private float $side) {}
public function area(): float { return $this->side ** 2; }
}
echo (new Square(3))->area(), "\n"; // 9 interface Point {
x: number;
y: number;
}
// Any object with x and y IS a Point — no 'implements' needed:
function distance(point: Point): number {
return Math.sqrt(point.x ** 2 + point.y ** 2);
}
console.log(distance({ x: 3, y: 4 })); // 5 TypeScript interfaces describe the shape of data, and typing is structural: any value with the right properties satisfies the interface automatically, with no
implements clause. This is compile-time duck typing — a fundamentally different model from PHP's nominal interfaces, which a class must explicitly declare it implements.Optional & Readonly Properties
<?php
class UserAccount {
public function __construct(
public readonly string $name,
public ?string $email = null, // optional-ish via nullable default
) {}
}
$account = new UserAccount("Alice");
echo $account->name, " ", $account->email ?? "no email", "\n"; interface UserAccount {
readonly name: string;
email?: string; // optional property — may be absent
}
const account: UserAccount = { name: "Alice" };
console.log(account.name, account.email ?? "no email");
// account.name = "Bob"; // compile error — readonly A trailing
? marks a property optional (it may be missing entirely, becoming undefined), and readonly forbids reassignment after construction — the direct parallel of PHP 8.1's readonly. Optionality is part of the type, so the compiler forces you to handle the possibly-absent value.Type Aliases & Intersections
<?php
// PHP has no type aliases; you repeat the union or use a class:
function format(int|float $amount, string $currency): string {
return $currency . number_format($amount, 2);
}
echo format(9.5, "$"), "\n"; // $9.50 type Money = number;
type Named = { name: string };
type Timestamped = { createdAt: number };
type Entity = Named & Timestamped; // intersection — has BOTH shapes
const user: Entity = { name: "Alice", createdAt: 1718000000 };
console.log(user.name, user.createdAt); The
type keyword names any type — a union, an object shape, or a primitive alias like Money — which PHP cannot do. The & intersection combines shapes so a value must satisfy all of them, the complement to the | union.Union Types & Narrowing
Type Narrowing
<?php
function lengthOf(int|string $value): int {
if (is_string($value)) {
return strlen($value); // PHP knows it's a string here
}
return $value; // ...and an int here
}
echo lengthOf("hello"), "\n"; // 5
echo lengthOf(42), "\n"; // 42 function lengthOf(value: number | string): number {
if (typeof value === "string") {
return value.length; // narrowed to string
}
return value; // narrowed to number
}
console.log(lengthOf("hello")); // 5
console.log(lengthOf(42)); // 42 TypeScript narrows a union type inside a
typeof (or instanceof, or property) check: after typeof value === "string", the compiler treats value as a string and lets you call .length. It is the same control-flow reasoning PHP's is_string guides, but the compiler enforces that every branch is type-correct.Discriminated Unions
<?php
// PHP models variants with a class hierarchy or a tagged array:
function area(array $shape): float {
return match ($shape["kind"]) {
"circle" => 3.14159 * $shape["radius"] ** 2,
"square" => $shape["side"] ** 2,
};
}
echo area(["kind" => "square", "side" => 4]), "\n"; // 16 type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return 3.14159 * shape.radius ** 2;
case "square": return shape.side ** 2;
}
}
console.log(area({ kind: "square", side: 4 })); // 16 A discriminated union tags each variant with a literal field (
kind); switching on that tag narrows to the right shape, so shape.radius is only accessible in the circle branch. The compiler verifies you handled every case — a far safer, type-checked version of PHP's tagged-array match.Functions
Functions & Arrows
<?php
function add(int $first, int $second): int {
return $first + $second;
}
$multiply = fn(int $first, int $second): int => $first * $second;
echo add(2, 3), "\n"; // 5
echo $multiply(2, 3), "\n"; // 6 function add(first: number, second: number): number {
return first + second;
}
const multiply = (first: number, second: number): number => first * second;
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6 Parameter types follow each name and the return type follows the parameter list after a colon. The arrow function
(a, b) => a * b is the counterpart to PHP's fn() => and, like it, captures surrounding variables automatically with no use clause.Optional, Default & Rest Params
<?php
function connect(string $host, int $port = 5432, string ...$flags): string {
return "{$host}:{$port} [" . implode(",", $flags) . "]";
}
echo connect("db", 5432, "ssl", "pool"), "\n"; function connect(host: string, port = 5432, ...flags: string[]): string {
return `${host}:${port} [${flags.join(",")}]`;
}
console.log(connect("db", 5432, "ssl", "pool")); Default parameters work as in PHP, and
...flags collects the rest into a typed array — the parallel of PHP's ...$flags. TypeScript also has a trailing ? for an optional parameter (name?: string), which makes the value string | undefined rather than supplying a default.Generics
Generic Functions
<?php
// PHP has no generics in the language — you accept mixed and lose type info,
// or document the type with a @template docblock for static analysers:
/** @template T @param T[] $items @return T */
function firstItem(array $items): mixed {
return $items[0];
}
echo firstItem([10, 20, 30]), "\n"; // 10 function firstItem<T>(items: T[]): T {
return items[0];
}
const firstNumber = firstItem([10, 20, 30]); // typed as number
const firstWord = firstItem(["a", "b"]); // typed as string
console.log(firstNumber, firstWord); // 10 a Generics are a genuine TypeScript feature PHP lacks at the language level:
<T> is a type variable, so firstItem([10, 20]) returns a number and firstItem(["a"]) a string, each fully checked. PHP can only approximate this with @template docblocks read by tools like Psalm or PHPStan.Constraints & Generic Types
<?php
// PHP collections are just arrays; element types live only in docblocks:
/** @param array<string,int> $scores */
function topScore(array $scores): int {
return max($scores);
}
echo topScore(["alice" => 90, "bob" => 85]), "\n"; // 90 function longest<T extends { length: number }>(items: T[]): T {
return items.reduce((best, item) =>
item.length > best.length ? item : best);
}
console.log(longest(["a", "bbb", "cc"])); // bbb
console.log(longest([[1], [1, 2, 3], [1, 2]])); // [ 1, 2, 3 ] A constraint
T extends { length: number } limits the generic to types that have a length, so the body can safely read it. Built-in generic types like Array<T>, Map<K, V>, and Record<K, V> give the typed collections PHP can only describe in comments.Classes & OOP
Defining a Class
<?php
class Greeter {
public function __construct(private string $name) {}
public function greet(): string {
return "Hello, {$this->name}!";
}
}
$greeter = new Greeter("Alice");
echo $greeter->greet(), "\n"; class Greeter {
constructor(private name: string) {} // parameter property
greet(): string {
return `Hello, ${this.name}!`;
}
}
const greeter = new Greeter("Alice");
console.log(greeter.greet()); TypeScript's parameter properties —
constructor(private name: string) — declare and assign a field in one line, exactly like PHP 8's constructor property promotion. Member access uses a dot, and this (lowercase, no $) plays the role of $this.Inheritance
<?php
abstract class Animal {
public function __construct(protected string $name) {}
abstract public function speak(): string;
}
class Dog extends Animal {
public function speak(): string {
return "{$this->name} says Woof";
}
}
echo (new Dog("Rex"))->speak(), "\n"; abstract class Animal {
constructor(protected name: string) {}
abstract speak(): string;
}
class Dog extends Animal {
speak(): string {
return `${this.name} says Woof`;
}
}
console.log(new Dog("Rex").speak()); Inheritance reads almost identically:
extends, abstract classes and methods, and protected visibility all carry over. Call the parent with super.method() (PHP's parent::) and the parent constructor with super(...).Implementing Interfaces
<?php
interface Jsonable {
public function toJson(): string;
}
class Point implements Jsonable {
public function __construct(public int $x, public int $y) {}
public function toJson(): string {
return json_encode(["x" => $this->x, "y" => $this->y]);
}
}
echo (new Point(3, 4))->toJson(), "\n"; interface Jsonable {
toJson(): string;
}
class Point implements Jsonable {
constructor(public x: number, public y: number) {}
toJson(): string {
return JSON.stringify({ x: this.x, y: this.y });
}
}
console.log(new Point(3, 4).toJson()); A class can explicitly
implements an interface for a checked contract, just as in PHP — but remember structural typing means a class need not declare implements to be usable where the interface is expected. The implements clause simply asks the compiler to verify the class really has the shape.Enums & Literal Types
Enums
<?php
enum Status: string {
case Active = "active";
case Inactive = "inactive";
}
$current = Status::Active;
echo $current->value, "\n"; // active enum Status {
Active = "active",
Inactive = "inactive",
}
const current: Status = Status.Active;
console.log(current); // active TypeScript enums and PHP 8.1 backed enums are close cousins: both name a fixed set of cases with backing values, read via
Status.Active / Status::Active. A difference to note: a TypeScript enum emits real runtime code (it is not erased), whereas most TS types vanish at compile time.Literal Union Types
<?php
// PHP uses an enum or validated strings for a fixed set:
enum Direction: string {
case North = "north";
case South = "south";
}
function move(Direction $direction): string {
return "moving " . $direction->value;
}
echo move(Direction::North), "\n"; // A union of string literals — lighter than an enum, fully checked:
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction): string {
return "moving " + direction;
}
console.log(move("north"));
// move("up"); // compile error — not a Direction TypeScript often models a fixed set with a literal union type like
"north" | "south" instead of an enum — it adds no runtime code and any plain string in the set is accepted, while anything outside it is a compile error. This idiom has no PHP equivalent; PHP reaches for an enum.Error Handling
Try / Catch / Finally
<?php
function parseAmount(string $input): int {
if (!is_numeric($input)) {
throw new InvalidArgumentException("not a number: {$input}");
}
return (int) $input;
}
try {
parseAmount("abc");
} catch (InvalidArgumentException $error) {
echo "Caught: ", $error->getMessage(), "\n";
} finally {
echo "Done\n";
} function parseAmount(input: string): number {
const value = Number(input);
if (Number.isNaN(value)) {
throw new Error(`not a number: ${input}`);
}
return value;
}
try {
parseAmount("abc");
} catch (error) {
console.log("Caught: " + (error as Error).message);
} finally {
console.log("Done");
} The
try/catch/finally structure matches PHP's, but with a twist: in TypeScript the caught value is typed unknown (because JavaScript can throw any value, not just exceptions), so you narrow or assert it — (error as Error).message — before reading .message.Custom Error Classes
<?php
class ValidationError extends Exception {}
function validate(int $age): void {
if ($age < 0) {
throw new ValidationError("age cannot be negative");
}
}
try {
validate(-1);
} catch (ValidationError $error) {
echo $error->getMessage(), "\n";
} class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
function validate(age: number): void {
if (age < 0) {
throw new ValidationError("age cannot be negative");
}
}
try {
validate(-1);
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.message);
}
} Custom errors subclass
Error (PHP's Exception), but you usually catch a single error and discriminate with instanceof, since JavaScript's catch cannot filter by type the way PHP's typed catch (ValidationError $error) can. Setting this.name is conventional for readable stack traces.Async & Promises
Async & Await
<?php
// PHP runs synchronously; there is no language-level async. A function
// just returns its value and the script blocks until it does:
function fetchValue(): int {
return 42; // imagine slow I/O here
}
echo fetchValue(), "\n"; // 42 async function fetchValue(): Promise<number> {
await new Promise((resolve) => setTimeout(resolve, 10));
return 42;
}
async function main(): Promise<void> {
const value = await fetchValue(); // await inside an async function
console.log(value); // 42
}
main(); Async is built into the language and its types: an
async function returns a Promise<T>, and await unwraps it without blocking the single thread. PHP has no native equivalent — concurrency needs Fibers or an extension like Swoole — so this is a real capability gap, with the type system tracking the Promise for you. (Modern runtimes also allow await at the top level of a module.)Awaiting Many Promises
<?php
// Classic PHP runs these one after another — no built-in Promise.all:
function square(int $n): int { return $n * $n; }
$results = array_map('square', [1, 2, 3]);
print_r($results); // 1, 4, 9 async function square(n: number): Promise<number> {
await new Promise((resolve) => setTimeout(resolve, 10));
return n * n;
}
async function main(): Promise<void> {
const results = await Promise.all([square(1), square(2), square(3)]);
console.log(results); // [ 1, 4, 9 ]
}
main(); Promise.all runs several async operations concurrently and resolves to an array of their results — structured concurrency PHP can only reach with an event-loop library. TypeScript types the result precisely: Promise.all over Promise<number> values yields a number[].⚠ Gotchas for PHP Devs
Types Are Not Runtime Checks
<?php
// PHP enforces declared types at RUNTIME:
function needInt(int $value): int {
return $value;
}
try {
needInt("not an int"); // TypeError at runtime
} catch (TypeError $error) {
echo "PHP caught it\n";
} function needNumber(value: number): number {
return value;
}
// TypeScript checks this at COMPILE time only. If a wrong type sneaks in
// from untyped data (JSON, any), nothing throws at runtime:
const fromJson: any = "not a number";
console.log(needNumber(fromJson) + 1); // "not a number1" — no error! The most important mental shift: TypeScript types are erased and never checked while the program runs. Bad data entering through
any, JSON.parse, or a network response will not throw a type error the way PHP's runtime-checked declarations would. Validate external data at the boundary (e.g. with Zod) — the compiler cannot.=== and Type Coercion
<?php
// PHP: == is loose, === is strict (PHP 8 fixed the worst coercions):
var_dump("1" == 1); // true (loose)
var_dump("1" === 1); // false (strict)
var_dump(0 == "abc"); // false in PHP 8 // JavaScript: == coerces unpredictably, so ALWAYS use ===:
console.log("1" == 1); // true (coerces — avoid)
console.log("1" === 1); // false (strict — prefer)
console.log(0 == ""); // true (a classic == trap)
console.log(0 === ""); // false Both languages have loose
== and strict ===, and the advice is the same: prefer ===. JavaScript's == coercion rules are even more surprising than PHP's (and PHP 8 already tamed its worst cases), so treat == as a code smell and reach for === by default.null vs undefined
<?php
// PHP has exactly ONE empty value: null
$data = ["name" => "Alice"];
echo $data["name"] ?? "missing", "\n"; // Alice
echo $data["email"] ?? "missing", "\n"; // missing
$value = null;
var_dump($value === null); // true // TypeScript has TWO: null and undefined
const data: { name: string; email?: string } = { name: "Alice" };
console.log(data.name ?? "missing"); // Alice
console.log(data.email ?? "missing"); // missing (absent → undefined)
let value: number | undefined;
console.log(value === undefined); // true — never set Where PHP has a single
null, JavaScript has two empties: undefined (a variable or property never given a value) and null (an explicit "no value"). A missing object property reads as undefined, not null. Both ?? and ?. treat the two together, which is why they are the safe tools here.Arrays Are References
<?php
// PHP arrays are value types — assignment COPIES:
$original = [1, 2, 3];
$copy = $original;
$copy[] = 4;
echo count($original), "\n"; // 3 — unchanged // JS arrays/objects are references — assignment ALIASES:
const original = [1, 2, 3];
const alias = original;
alias.push(4);
console.log(original.length); // 4 — changed!
const realCopy = [...original]; // spread makes a shallow copy
realCopy.push(5);
console.log(original.length); // still 4 A crucial reversal: PHP arrays are value types, so
$copy = $original duplicates them, whereas JavaScript arrays and objects are references, so alias = original makes two names for one value. Copy with the spread operator [...original] / {...object}, remembering it is shallow.