Monday, 29 September 2025

TypeScript late static binding: parameters that aren't actually parameters

G'day:

The links to the code in this won't work as yet, as I'm still working on the underlying… work.

Well that was a clumsy sentence.

I've been working through classes in TypeScript as part of my learning project, and today I hit static methods. Coming from PHP, one of the first questions that popped into my head was "how does late static binding work here?"

In PHP, you can do this:

class Base {
    static function create() {
        return new static();  // Creates instance of the actual called class
    }
}

class Child extends Base {}

$instance = Child::create();  // Returns a Child instance, not Base

The static keyword in new static() means "whatever class this method was actually called on", not "the class where this method is defined". It's late binding - the class is resolved at runtime based on how the method was called.

Seemed like a reasonable thing to want in TypeScript. Turns out it's possible, but the syntax is... questionable.

The TypeScript approach

Here's what I ended up with:

export class TranslatedNumber {
  constructor(
    private value: number,
    private en: string,
    private mi: string
  ) {}

  getAll(): { value: number; en: string; mi: string } {
    return {
      value: this.value,
      en: this.en,
      mi: this.mi,
    }
  }

  static fromTuple<T extends typeof TranslatedNumber>(
    this: T,
    values: [value: number, en: string, mi: string]
  ): InstanceType<T> {
    return new this(...values) as InstanceType<T>
  }
}

export class ShoutyTranslatedNumber extends TranslatedNumber {
  constructor(value: number, en: string, mi: string) {
    super(value, en.toUpperCase(), mi.toUpperCase())
  }
}

(from static.ts)

And it works - when you call ShoutyTranslatedNumber.fromTuple(), you get a ShoutyTranslatedNumber instance back, not a TranslatedNumber:

const translated = ShoutyTranslatedNumber.fromTuple([3, 'three', 'toru'])

expect(translated.getAll()).toEqual({
  value: 3,
  en: 'THREE',
  mi: 'TORU',
})

(from static.test.ts)

The late binding works. But look at that fromTuple method signature again. Specifically this bit: this: T.

Parameters that aren't parameters

When I first saw this: T in the parameter list, my immediate reaction was "okay, so I need to pass the class as the first argument?"

But the usage doesn't have any extra parameter:

const translated = ShoutyTranslatedNumber.fromTuple([3, 'three', 'toru'])

No class being passed. Just the tuple. So what the hell is this: T, doing in the parameter list?

Turns out it's a TypeScript-specific construct that exists purely for the type system. It's not a runtime parameter at all - it gets completely erased during compilation. It's a type hint that tells TypeScript "remember which class this static method was called on".

When you write ShoutyTranslatedNumber.fromTuple([3, 'three', 'toru']), TypeScript infers:

  • The this inside fromTuple refers to ShoutyTranslatedNumber
  • Therefore T is typeof ShoutyTranslatedNumber
  • Therefore InstanceType<T> is ShoutyTranslatedNumber

It's clever. It works. But it's also completely bizarre if you're coming from any language where parameters are just parameters.

Why this feels wrong

The thing that bothers me about this isn't that it doesn't work - it does work fine. It's that the solution is a hack at the type system level when it should be a language feature.

TypeScript could have introduced syntax like new static() or new this() and compiled it to whatever JavaScript pattern makes it work at runtime. Instead, they've made developers express "the class this method was called on" through a phantom parameter that only exists for the type checker.

Compare this to how other languages handle it:

PHP just gives you static as a keyword. You write new static() and the compiler handles the rest.

Kotlin compiles to JavaScript too, but when you write Kotlin, you write actual Kotlin - proper classes, sealed classes, data classes, all the language features. The compiler figures out how to make it work in JavaScript. You don't write weird pseudo-parameters because "JavaScript doesn't have that feature".

TypeScript has positioned itself as "JavaScript with types" rather than "a language that compiles to JavaScript", which means it's constantly constrained by JavaScript's limitations instead of abstracting them away. When JavaScript doesn't have a concept, TypeScript makes you do the workaround instead of the compiler doing it.

It's functional, but it's not elegant. And it's definitely not intuitive.

Does it matter?

In practice? Not really. Once you know the pattern, it's straightforward enough to use. The this: T parameter becomes just another TypeScript idiom you memorise and move on.

But it does highlight a fundamental tension in TypeScript's design philosophy. The language is scared to be a proper language with its own features and syntax. Everything has to map cleanly back to JavaScript, even when that makes the developer experience worse.

I found this Stack Overflow answer while researching this, which explains the mechanics well enough, but doesn't really acknowledge how weird the solution is. It's all type theory without much "here's why the language works this way".

For now, I've got late static binding working in TypeScript. It required some generics gymnastics and a phantom parameter, but it does what I need. I'll probably dig deeper into generics in a future ticket - there's clearly more to understand there, and I've not worked with generics in any language before, so that'll be interesting.

The code for this is in my learning-typescript repository if you want to see the full implementation. Thanks to Claudia for helping me understand what the hell this: T was actually doing and for assistance with this write-up.

Righto.

--
Adam