Capítulo 13 de 36

Chapter 13: Classes — Static Members, Generics, this, and Abstract Classes

Core Idea

Beyond instance members, classes carry a second, separate type surface for static members, a dynamic this type that tracks subclasses automatically, and abstract as the mechanism for base classes that can never be instantiated directly.

Key Concepts

  • static members: attached to the constructor object itself (MyClass.x), accessed without new. Support the same public/protected/private modifiers and are inherited by subclasses. A few names (name, length, call, …) are reserved because classes are themselves callable Function objects and would collide with the Function prototype.
  • No "static classes": TypeScript doesn't need a dedicated construct for "a class with only static members" the way languages that force everything into a class do — a plain object literal or a top-level function serves the same purpose more simply.
  • static blocks: a static { ... } block runs once at class-definition time with its own scope, and can read/write the class's private/#-prefixed static fields — useful for initialization logic (e.g. reading from an external source) that's too complex for a single field initializer expression.
  • Generic classes: class Box<Type> { contents: Type } — type parameters infer from new Box("hello") the same way they infer in a generic function call. A generic class's static members can never reference the class's type parameter — there's only one underlying static property slot at runtime, shared across every instantiation (Box<string> and Box<number> would otherwise silently share and clobber the same static field).
  • this at runtime is call-site-dependent, same as plain JS — extracting a method off an instance (const g = c.getName) and calling it loses the original this binding. Two TS-level mitigations:
    • Arrow function property (getName = () => this.name): guarantees correct this even from un-checked JS callers, at the cost of one function allocation per instance and losing access to super.getName in a subclass (arrow functions have no entry in the prototype chain to look it up from).
    • this parameter (getName(this: MyClass) { ... }): a compile-time-only declared parameter (erased on emit) that makes calling the method with a wrong this a type error at the call site — one function per class (not per instance), and super calls still work, but only protects TS-checked callers.
  • this as a return/parameter type: a method returning this (inferred automatically for fluent/chainable APIs like .set(value) { ...; return this }) automatically returns the subclass's type when called on a subclass instance — more precise than hardcoding the base class name as the return type. Using this as a parameter type (sameAs(other: this)) similarly restricts the argument to instances of the same (sub)class, not just the base.
  • this is Type type guards: a class method can narrow this the same way a free function narrows a parameter with x is TypeisFile(): this is FileRep. A common use is lazy-validated optional fields: hasValue(): this is { value: T } strips undefined from value's type inside the narrowed branch.
  • Parameter properties: prefixing a constructor parameter with a visibility modifier (public, private, protected, or readonly) both declares a field of that name/type/visibility and assigns it from the argument — constructor(public readonly x: number, private y: number) needs no body at all for that assignment.
  • Class expressions: anonymous class literals (const C = class<Type> { ... }), generic-capable just like class declarations.
  • InstanceType<typeof SomeClass>: the utility type for "the type of an instance produced by this class," derived from the class's own constructor type via typeof.
  • abstract classes/members: an abstract class can't be instantiated with new directly; abstract methods/fields declare a signature with no implementation, and every concrete subclass must implement them (a missing implementation is a compile error). To accept "a constructor that produces a (subclass of) some abstract class" as a parameter — a common factory pattern — type the parameter as a construct signature (new () => Base), not typeof Base: typeof Base would also (wrongly) accept the abstract class itself, which can't actually be new'd.
  • Structural class comparisons: like all TypeScript types, classes compare structurally, not nominally — two unrelated classes with identical member shapes are mutually assignable, and subtype relationships exist even without explicit extends. An empty class (class Empty {}) has no members to check, so — in a structural system — it becomes a supertype of nearly everything, meaning almost any value satisfies it. This is presented as a cautionary "don't do this," not a recommended pattern.

Code Examples

class Box {
  contents = "";
  set(value: string) {
    this.contents = value;
    return this; // inferred return type: `this`, not `Box`
  }
}
class ClearableBox extends Box {
  clear() { this.contents = ""; }
}
const b = new ClearableBox().set("hi"); // b: ClearableBox, not Box
  • What it demonstrates: the this return type automatically tracking the actual subclass, enabling type-safe method chaining across an inheritance hierarchy.
abstract class Base {
  abstract getName(): string;
  printName() { console.log("Hello, " + this.getName()); }
}
class Derived extends Base {
  getName() { return "world"; }
}
function greet(ctor: new () => Base) { // construct signature, NOT `typeof Base`
  new ctor().printName();
}
greet(Derived); // OK
greet(Base);    // Error — Base is abstract, can't be `new`'d
  • What it demonstrates: accepting "a concrete constructor for a Base subtype" safely via a construct-signature parameter, rejecting the abstract base itself.

Reference Tables

this-safety techniquethis correctnessMemorysuper access
Arrow function propertyguaranteed, even for untyped JS callersone closure per instancenot available
this: T parameterenforced only for TS-checked call sitesone function per classavailable

Key Takeaways

  1. Never give a generic class's static members a type depending on the class's own type parameter — there's exactly one shared static slot at runtime across every instantiation.
  2. Use construct-signature parameters (new () => T), not typeof AbstractClass, when a function needs "a constructor for T" but must reject abstract bases.
  3. Prefer letting TS infer this as a chainable method's return type over hardcoding the base class name — it keeps fluent APIs correct across subclasses for free.
  4. Choose the arrow-function-property vs. this-parameter trade-off deliberately: arrow properties are safer against untyped callers but cost memory-per-instance and lose super; this parameters are cheaper and keep super but only protect TS-checked call sites.

Connects To

  • Classes — Members, Heritage & Visibility (previous chapter): visibility modifiers combine directly with parameter properties here.
  • Generics: the class-level generics mechanics mirror generic functions/interfaces almost exactly.
  • Narrowing: this is Type is the class-method form of the free-function type predicate.
  • Utility Types (Reference): InstanceType<T> is the general-purpose version of the class-to-instance-type mapping shown here.