PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

TypeScript for the PHP developer – types, interfaces, generics, async/await

by Henryk Tews / Tuesday, 10 January 2023 / Published in JavaScript

TypeScript is to JavaScript what strict_types + PHPStan is to PHP – it adds a type system on top of a dynamically typed language and catches errors before runtime. For a PHP developer the concepts are familiar: interfaces, generics, union types. I show the most important TypeScript features from the perspective of someone who thinks in typed PHP daily.

Basic types – familiar territory

// TypeScript type annotations look like PHP type hints
function formatPrice(amount: number, currency: string = 'PLN'): string {
    return amount.toFixed(2) + ' ' + currency;
}

// Nullable - same idea as PHP's ?string
function findProduct(id: number): Product | null {
    return products.find(p => p.id === id) ?? null;
}

// Union types - like PHP 8.0's int|string
function processId(id: number | string): string {
    return typeof id === 'number' ? `#${id}` : id;
}

// Readonly - like PHP's readonly properties
const config: { readonly apiUrl: string; timeout: number } = {
    apiUrl: 'https://api.example.com',
    timeout: 30,
};
// config.apiUrl = 'other'; // Error: Cannot assign to 'apiUrl' because it is read-only

Interfaces – the TypeScript contract

// TypeScript interface - structural typing (duck typing with checks)
interface Product {
    id: number;
    sku: string;
    name: string;
    price: number;
    isActive: boolean;
    category?: string; // optional property (like PHP's ?string with default null)
}

// Extending interfaces - like PHP implements + extends
interface PhysicalProduct extends Product {
    weight: number;
    dimensions: { width: number; height: number; depth: number };
}

interface DigitalProduct extends Product {
    downloadUrl: string;
    expiresAt: Date | null;
}

// Implementing interface in a class
class SimpleProduct implements Product {
    constructor(
        public id: number,
        public sku: string,
        public name: string,
        public price: number,
        public isActive: boolean = true
    ) {}
}

// Function accepting any Product implementation
function displayProduct(product: Product): void {
    console.log(`${product.sku}: ${product.name} - ${product.price} PLN`);
}

Generics – like PHP templates

// Generic function - works with any type while preserving type info
function first<T>(items: T[]): T | undefined {
    return items[0];
}

const firstProduct  = first<Product>([product1, product2]); // Product | undefined
const firstNumber   = first<number>([1, 2, 3]);             // number | undefined
// TypeScript infers T from the argument if you omit <T>

// Generic class - like a typed PHP collection
class TypedCollection<T> {
    private items: T[] = [];

    add(item: T): void { this.items.push(item); }
    get(index: number): T { return this.items[index]; }
    all(): T[] { return [...this.items]; }
    count(): number { return this.items.length; }

    filter(predicate: (item: T) => boolean): TypedCollection<T> {
        const result = new TypedCollection<T>();
        this.items.filter(predicate).forEach(item => result.add(item));
        return result;
    }
}

const products = new TypedCollection<Product>();
products.add({ id: 1, sku: 'A', name: 'Test', price: 9.99, isActive: true });
const active = products.filter(p => p.isActive); // TypedCollection<Product>

async/await – familiar from PHP 8.1 Fibers context

// async/await is TypeScript's (and JS's) way to handle asynchronous operations
// The concept is similar to PHP's Fibers but built into the language

interface ApiResponse<T> {
    data: T;
    meta: { total: number; page: number };
}

async function fetchProducts(page: number = 1): Promise<ApiResponse<Product[]>> {
    const response = await fetch(`/rest/V1/products?searchCriteria[currentPage]=${page}`);

    if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
    }

    return response.json();
}

// Error handling with async/await
async function loadProductPage(categoryId: number): Promise<Product[]> {
    try {
        const { data, meta } = await fetchProducts(1);
        console.log(`Loaded ${data.length} of ${meta.total} products`);
        return data;
    } catch (error) {
        if (error instanceof Error) {
            console.error('Failed to load products:', error.message);
        }
        return [];
    }
}

// Parallel execution - equivalent to running multiple promises concurrently
async function loadDashboard(): Promise<void> {
    const [products, categories, orders] = await Promise.all([
        fetchProducts(),
        fetchCategories(),
        fetchRecentOrders(),
    ]);
    // All three requests run in parallel
    console.log(products, categories, orders);
}

Type utilities – practical patterns

// Partial<T> - all properties optional (useful for update payloads)
async function updateProduct(id: number, changes: Partial<Product>): Promise<Product> {
    const response = await fetch(`/rest/V1/products/${id}`, {
        method: 'PUT',
        body: JSON.stringify(changes),
    });
    return response.json();
}

updateProduct(42, { price: 79.99 }); // only price, everything else optional

// Pick<T, K> - select specific properties
type ProductSummary = Pick<Product, 'id' | 'sku' | 'name' | 'price'>;

// Omit<T, K> - exclude specific properties
type ProductWithoutId = Omit<Product, 'id'>;

// Record<K, V> - typed object/dictionary
const productIndex: Record<string, Product> = {};
productIndex['SKU-001'] = product; // sku as key

// Enum in TypeScript
enum OrderStatus {
    Pending    = 'pending',
    Processing = 'processing',
    Complete   = 'complete',
    Cancelled  = 'cancelled',
}

function getStatusLabel(status: OrderStatus): string {
    const labels: Record<OrderStatus, string> = {
        [OrderStatus.Pending]:    'Awaiting payment',
        [OrderStatus.Processing]: 'In progress',
        [OrderStatus.Complete]:   'Completed',
        [OrderStatus.Cancelled]:  'Cancelled',
    };
    return labels[status];
}

TypeScript in Magento 2 context

// TypeScript compiles to JavaScript - Magento's RequireJS uses plain JS
// But you can write TypeScript in Hyvä Theme or custom Vue/React components

// Example: typed API client for Magento REST
interface MagentoProduct {
    id: number;
    sku: string;
    name: string;
    price: number;
    status: 1 | 2; // 1=enabled, 2=disabled
}

interface SearchResult<T> {
    items: T[];
    search_criteria: object;
    total_count: number;
}

async function getMagentoProducts(
    storeCode: string = 'default'
): Promise<SearchResult<MagentoProduct>> {
    const url = `/rest/${storeCode}/V1/products?searchCriteria[pageSize]=20`;
    const res = await fetch(url, {
        headers: { Authorization: `Bearer ${getToken()}` }
    });
    return res.json() as Promise<SearchResult<MagentoProduct>>;
}

Summary

TypeScript makes JavaScript feel much closer to typed PHP. Interfaces define contracts, generics allow type-safe collections, async/await makes asynchronous code readable. For a PHP developer the biggest mental shift is not the types – it is accepting that TypeScript types are erased at runtime (no runtime type checking by default). PHPStan equivalent for TypeScript would be running tsc – the TypeScript compiler – as a CI check.

About Henryk Tews

What you can read next

Vue.js for the PHP developer – Options API, Composition API, Magento REST communication
React.js – JSX, useState, useEffect, custom hooks, comparison with Vue
Next.js for the PHP developer – SSR, Server Components, GraphQL with Magento

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}