Locutus 3.0: Fully Assimilated into TypeScript

It’s been 18 years since I pushed the first commit to this project, and nearly 10 since we renamed php.js to Locutus and expanded beyond PHP. Back then I wrote that the project had found itself in a dead-end street, and that I needed to make changes to get my mojo back. It worked. We kept going.

This time, the push came from the community. In February, @SignpostMarv opened Discussion #532 asking about TypeScript definitions. The @types/locutus package on DefinitelyTyped had drifted out of sync, and maintaining separate type definitions for 500 functions across 12 languages is nobody’s idea of a good time. SignpostMarv laid out two options: add .d.ts files alongside the JavaScript, or go all in and convert the entire codebase to TypeScript. My response was: let’s go all in.

So that’s what we did. PR #535 converts all 497 function implementations to TypeScript. Every one of them, across all 12 languages: PHP, Go, Python, Ruby, C, Perl, Lua, R, Julia, Elixir, Clojure, and AWK. The src/ directory now contains 553 .ts files and zero .js files.

I’ll be honest: a rewrite of this scale would have taken months by hand. I used Claude to help with the heavy lifting of the conversion, and then spent time reviewing, testing, and fixing what came out the other end. The build still passes all 494 test suites. If you’re curious how it went, the full history is in the PR.

What’s new

Types ship with the package. No more @types/locutus. When you import { sprintf } from 'locutus/php/strings/sprintf', your editor knows the signature. Runtime code and type definitions now come from the same source.

Named exports everywhere. Functions are exported by name instead of as default exports. This is the main breaking change and the one you’ll need to update your code for.

The website got better too. Function pages now show code in multiple variants (Module/Standalone, TypeScript/JavaScript) with copy buttons, so you can grab exactly the version you need.

Stronger guardrails for contributors. CI now rejects @ts-nocheck and @ts-ignore, enforces API signature stability, and keeps new .js additions on an explicit allowlist. The codebase should stay TypeScript from here.

Breaking changes

There are three things that will break when you upgrade.

1. Named exports replace default exports

Before:

import sort from 'locutus/php/array/sort'

After:

import { sort } from 'locutus/php/array/sort'

If you use CommonJS require, the same applies:

// before (broken in v3)
const sort = require('locutus/php/array/sort')

// after
const { sort } = require('locutus/php/array/sort')

2. Node 22+ required

"engines": {
"node": ">= 22"
}

3. var and net-gopher dropped from locutus/php root

ESM namespace export keys must be valid identifiers. var is a reserved keyword and net-gopher contains a hyphen, so neither can be exported from the locutus/php barrel. Use deep imports instead:

import * as phpVar from 'locutus/php/var'
import * as phpNetGopher from 'locutus/php/net-gopher'

Upgrade guide

npm install locutus@^3
# or
yarn add locutus@^3
  1. Ensure Node >=22 wherever Locutus runs.
  2. Find-and-replace default imports with named imports. In most codebases this is a straightforward search.
  3. If you access var or net-gopher through the locutus/php root, switch to deep imports.
  4. Run your typecheck and tests.

The published package now ships both ESM and CJS runtime outputs. import and require() both continue to work with named exports.

Thank you

This project has had 137 contributors over its lifetime. Special thanks to Brett Zamir for an extraordinary number of function implementations and reviews over the years, to Rafal Kukawski for sustained improvements, and to everyone who filed issues, sent PRs, or just used the project quietly in their own work. And to @SignpostMarv for the discussion that kicked this whole thing off and for the early reviews.

To the GitHubs!

Kevin

Share