PONY λ M2 Modula-2

PHP.CodeCompared.To/TypeScript

An interactive executable cheatsheet comparing PHP and TypeScript

PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM) TypeScript 6.0
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 propertiesconstructor(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.