Capítulo 12 de 36

Chapter 12: Classes — Members, Heritage & Visibility

Core Idea

TypeScript fully supports ES2015 class syntax and layers type annotations, visibility modifiers (public/protected/private), and compile-time-only contracts (implements) on top — enforcing that a derived class always stays a structural subtype of its base.

Key Concepts

  • Fields: x: number declares a public writable property (implicitly any if untyped). An initializer (x = 0) both sets a runtime default and lets TypeScript infer the field's type.
  • strictPropertyInitialization: requires every field to be assigned inside the constructor body itself — TS does not trace calls out to other methods invoked from the constructor (a subclass could override those and skip initialization). Use the definite-assignment assertion (name!: string) when something outside the constructor is responsible for initializing a field (e.g. a framework/DI container).
  • readonly fields: block reassignment outside the constructor — same compile-time-only guarantee as readonly on object type properties.
  • Constructors: mostly function-like (params, defaults, overloads) but can't have type parameters (those go on the class itself) or a return type annotation (always the instance type). Forgetting super() before touching this in a subclass is a compile error, not just a runtime footgun.
  • Methods access class members only via this. — an unqualified name inside a method body resolves to the enclosing scope, not implicitly to a class member of the same name.
  • Getters/setters: get x()/set x(v) behave like accessors. A getter with no matching setter makes the property automatically readonly. An unannotated setter parameter infers its type from the getter's return type. Since TS 4.3, getter and setter can have genuinely different types (e.g. a setter accepting string | number | boolean that normalizes into a getter that only ever returns number).
  • implements only checks compatibility — it never changes the class's inferred types. A method's parameter types are still inferred purely from the class body; implementing an interface with an optional property does not create that property on the class. This is a common source of surprise: implements is a validation, not a code-generation mechanism.
  • extends / overriding: a derived class must remain a subtype of its base — you can widen a parameter to optional (greet(name?: string)) but not narrow it to required, because any code holding a Base-typed reference to a Derived instance must still be able to call the base signature safely.
  • Initialization order (a common surprise): base class fields initialize → base constructor runs → derived class fields initialize → derived constructor runs. A base constructor reading this.someField sees the base class's own initializer value, not a subclass's override, because the subclass's field initializers haven't run yet.
  • declare on a field: when useDefineForClassFields/target >= ES2022 makes subclass field initializers overwrite inherited values, declare resident: Dog re-narrows an inherited field's type with zero runtime emit — pure type-only re-declaration.
  • Member visibility: public (default, accessible anywhere), protected (subclasses only — and not accessible across sibling subclasses of the same base, even though that might seem intuitively safe), private (not even visible to subclasses). All are compile-time only — plain JS property lookup (obj["secretKey"], or a .js consumer) can still read a private TS field; that's "soft private." Genuine "hard private" (unreadable even from JS, no bracket-notation escape hatch) requires JS's own #privateField syntax, which TS compiles to closures/WeakMaps on older targets.
  • Cross-instance private access is allowed — two instances of the same class can read each other's private members (TypeScript follows Java/C#/C++/Swift/PHP convention here, not Ruby's stricter model).

Code Examples

class Base {
  protected getName() { return "hi"; }
}
class Special extends Base {
  greet() { return "Howdy, " + this.getName(); } // OK: protected visible in subclass
}
  • What it demonstrates: protected members are usable from subclass method bodies but not from outside the hierarchy.
interface Checkable { check(name: string): boolean; }
class NameChecker implements Checkable {
  check(s) { return s.toLowerCase() === "ok"; } // s is inferred `any`, NOT `string`!
}
  • What it demonstrates: implements is a compatibility check only — it does not backfill parameter types from the interface into the class body.

Reference Tables

ModifierVisible fromCompile-time or runtime?
public (default)anywheren/a
protectedclass + subclasses only (not sibling subclasses)compile-time only
privatedeclaring class only (readable cross-instance of same class)compile-time only ("soft private")
#field (JS private)declaring class onlyruntime-enforced ("hard private")

Key Takeaways

  1. implements never changes a class's inferred types — always annotate method parameters explicitly even when an interface already constrains them.
  2. TS private/protected are erased at compile time and don't stop plain JS access — reach for JS's #field syntax (or closures/WeakMaps) when you actually need runtime-enforced privacy.
  3. Remember the base-before-derived field initialization order when a base constructor reads a field a subclass also declares — it will see the base's own initial value, not the override.
  4. A derived class's overridden method must remain callable everywhere the base method could be called (parameters can only widen, never narrow) — TS enforces this so a Base-typed reference to a Derived instance stays safe.

Connects To

  • Classes — Advanced (next chapter): static members, generics, this typing, parameter properties, and abstract classes.
  • Object Types: readonly, index signatures, and structural typing concepts reused directly on classes.
  • Narrowing: this is Type guards (advanced chapter) extend the type-predicate mechanism to class methods.