Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

TypeScript for PHP Developers: The Parts That Actually Matter

About Post

You type a parameter as number. The compiler is happy. And at runtime, adding a fee of 5 to 100 gives you "1005". If you come from PHP, that feels like a betrayal: you wrote the type, so why didn't anything enforce it?

If you come from modern PHP, TypeScript feels familiar very quickly: types on parameters, interfaces, union types, nullable values. But one difference sits underneath everything, and once you understand it, the rest of the language makes sense.

The big one: PHP checks at runtime, TypeScript checks before

In PHP, types are enforced while the code runs:

declare(strict_types=1);

function addFee(float $amount): float
{
    return $amount + 5;
}

addFee('100'); // TypeError, thrown at runtime

TypeScript checks types when you compile, and then erases them. The JavaScript that actually runs has no types at all:

function addFee(amount: number): number {
  return amount + 5;
}

addFee('100'); // compile error, good

const body = JSON.parse(input); // type: any
addFee(body.amount); // compiles fine, returns "1005" if amount is a string

The compiler trusted that body.amount was a number because any switches checking off. Nothing checked at runtime, because nothing exists at runtime to check. That's the "1005" bug.

So the mental model is: TypeScript types are promises you make to the compiler. Inside your code, the compiler holds you to them very well. At the edges, where data comes from outside (API responses, JSON, form input, local storage), nobody holds the outside world to them.

Validate at the boundary

This is the habit PHP developers need to bring with them. In Laravel you validate requests before using them. In TypeScript, do the same with incoming data. A library like Zod lets you declare a schema once and get both the runtime check and the type:

import { z } from 'zod';

const Contract = z.object({
  id: z.number(),
  reference: z.string(),
  endsAt: z.string().nullable(),
});

type Contract = z.infer<typeof Contract>;

const contract = Contract.parse(await res.json()); // throws if the shape is wrong

Writing await res.json() as Contract instead looks similar, but it's just a promise again. as checks nothing.

Interfaces describe shapes, not classes

In PHP, a class satisfies an interface only if it says implements. TypeScript uses structural typing: if an object has the right shape, it fits, no declaration needed.

interface HasEmail {
  email: string;
}

function sendWelcome(user: HasEmail) { /* ... */ }

const tenant = { email: '[email protected]', unit: 'A-12' };
sendWelcome(tenant); // fine: it has an email, no "implements" needed

sendWelcome({ email: '[email protected]', unit: 'A-12' }); // error, see below

The quirk in that last line: passing an object literal directly with extra properties is flagged (an "excess property check"), because an unexpected property written inline is usually a typo. The same object in a variable is fine. In practice you'll use interfaces (or type aliases, which are nearly interchangeable for object shapes) mostly to describe data, not to build class hierarchies.

Union types, with superpowers

PHP has had union types since 8.0: int|string. TypeScript has them too, plus literal types and narrowing: after a check, the compiler knows which member you have. Combine those and you get discriminated unions, one of TypeScript's best features:

type PaymentResult =
  | { status: 'paid'; receiptId: string }
  | { status: 'failed'; reason: string }
  | { status: 'pending' };

function describe(result: PaymentResult): string {
  switch (result.status) {
    case 'paid':
      return `Paid, receipt ${result.receiptId}`;
    case 'failed':
      return `Failed: ${result.reason}`;
    case 'pending':
      return 'Still processing';
  }
}

Inside case 'paid', accessing result.reason is a compile error. Add a fourth status later and the function stops compiling until you handle it. The closest PHP equivalent is an enum with a match, but PHP can't attach different fields to each case like this.

Generics: the thing PHP doesn't have

PHP has no native generics. You write @template in docblocks and let PHPStan or Psalm check them. In TypeScript they're part of the language:

interface Paginated<T> {
  data: T[];
  currentPage: number;
  lastPage: number;
}

async function getJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>; // a promise to the compiler, not a check
}

const page = await getJson<Paginated<Contract>>('/api/contracts');

Now page.data[0].reference autocompletes and is type-checked everywhere. Notice the comment, though: the generic is only as honest as the data. That's the boundary rule again.

Two kinds of nothing

PHP has null. JavaScript has null and undefined. Missing object properties and unset variables are undefined; APIs and databases usually give you null. In TypeScript, reference?: string means "might be missing", while reference: string | null means "always present, might be null". The good news: ?. and ?? work just like PHP's ?-> and ??.

Turn on strict mode. Day one.

Set "strict": true in tsconfig.json. Without it, null can sneak into any type and untyped parameters silently become any, which throws away most of the value. Adding strict mode to an existing project later is much more painful than starting with it.

And prefer unknown over any. Both accept any value, but unknown forces you to check before using it, while any turns the type checker off.

If you remember one thing: PHP types protect you at runtime. TypeScript types protect you while you write code. Neither protects you from bad data at the edges, so validate there in both languages.

Cheat sheet

PHPTypeScript
Types checked at runtimeTypes checked at compile time, then erased
implements requiredMatching shape is enough
int|stringnumber | string, plus literal types like 'paid'
?stringstring | null, or prop?: string for optional
Generics via PHPStan docblocksBuilt-in generics
mixedunknown (safe) or any (unchecked)
Form Request validationSchema validation (Zod or similar) at the boundary

The TypeScript Handbook is genuinely good, and with a PHP background you can skim the first half quickly.

If you've made the move from PHP to TypeScript, what tripped you up first? My bet is on trusting as more than it deserves.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close