Classes
|
This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
TypeScript classes are ES2015 classes (see Classes) plus a type layer: member type
annotations, visibility modifiers, abstract, implements, and override checking. Most of that layer is
erased at compile time, but parameter properties, #private members and static blocks emit real runtime code.
This page follows the handbook’s Classes chapter.
Fields, methods and accessors
Fields carry a type; methods and get/set accessors are annotated like functions. A field with an initializer
infers its type from that initializer.
class Circle {
radius: number;
readonly kind = "circle"; // literal type "circle", cannot be reassigned
label?: string; // optional: type is string | undefined
constructor(radius: number) {
this.radius = radius;
}
area(): number {
return Math.PI * this.radius ** 2;
}
get diameter(): number {
return this.radius * 2;
}
set diameter(value: number) {
this.radius = value / 2;
}
}
With strict on — specifically
strictPropertyInitialization — every
non-optional field must be assigned in its declaration or in the constructor. When a field is really initialized
elsewhere (a lifecycle hook, a shared init() helper), the definite-assignment assertion !: tells the
compiler to trust you.
class Config {
settings!: Record<string, string>; // "trust me, this is assigned before use"
constructor() {
this.settings = load();
}
}
Visibility: erased modifiers vs. hard privacy
public (the default), protected and private are compile-time only. They are checked and then erased — at
runtime the properties are ordinary and reachable through (obj as any).x. The ECMAScript #name form is real:
the JavaScript runtime itself forbids access from outside the class body.
class Account {
public owner: string;
protected balance = 0;
private pin: string;
#token = crypto.randomUUID(); // genuinely private at runtime
constructor(owner: string, pin: string) {
this.owner = owner;
this.pin = pin;
}
sameToken(other: Account): boolean {
return this.#token === other.#token; // OK: still inside the class body
}
}
const a = new Account("Ada", "0000");
a.balance; // Error: 'balance' is protected
a.pin; // Error: 'pin' is private
(a as any).pin; // compiles and works -- 'private' was erased
// a.#token; // SyntaxError at parse time -- hard privacy
Prefer #private when the boundary must hold at runtime; the private keyword is fine for plain intra-team
encapsulation and is friendlier to tests and debuggers.
static members and static blocks
static attaches a member to the class object rather than to instances. A static block runs once when the
class is initialized and can see private static state.
class Registry {
static readonly items: string[] = [];
static #count = 0;
static {
Registry.items.push("default");
Registry.#count = Registry.items.length;
}
static get count(): number {
return Registry.#count;
}
}
abstract classes and members
An abstract class cannot be instantiated directly. abstract methods and fields have no body and must be
implemented by concrete subclasses; concrete methods on the abstract class may call the abstract ones.
abstract class Shape {
abstract area(): number;
describe(): string {
return `area is ${this.area().toFixed(2)}`;
}
}
class Square extends Shape {
constructor(private readonly side: number) {
super();
}
area(): number {
return this.side ** 2;
}
}
new Shape(); // Error: cannot create an instance of an abstract class
Parameter properties
Declaring a constructor parameter with a visibility modifier (or readonly) both takes the argument and
declares-and-assigns a field of the same name. constructor(private readonly x: number) replaces a field
declaration, a parameter, and a this.x = x line. See
Parameter Properties.
class Vector {
constructor(
public readonly x: number,
public readonly y: number,
) {}
plus(other: Vector): Vector {
return new Vector(this.x + other.x, this.y + other.y);
}
}
new Vector(1, 2).plus(new Vector(3, 4)); // Vector { x: 4, y: 6 }
Unlike type annotations, parameter properties emit runtime code — the compiler generates the field
assignments — so they are not erasable syntax. A file that uses them cannot be handled by a pure type-stripping
step (--erasableSyntaxOnly, Node’s built-in type stripping, some fast loaders); write the field and the
assignment out by hand if every construct must be erasable.
implements vs. extends, and override
extends inherits implementation from one base class. implements only adds a check that the class is
assignable to one or more interfaces — it contributes nothing to the body and does not infer or fill in members.
interface Serializable {
toJSON(): string;
}
class Session extends EventTarget implements Serializable {
constructor(private readonly id: string) {
super();
}
toJSON(): string {
return JSON.stringify({ id: this.id });
}
}
When overriding an inherited member, mark it override. With
noImplicitOverride on, redefining an inherited
member without override is an error, and an override member whose base member later disappears is flagged
too — both directions stay honest as the base class evolves.
class Base {
greet(): string {
return "hi";
}
}
class Loud extends Base {
override greet(): string {
return super.greet().toUpperCase();
}
}
A class is both a value and a type
Declaring class Duration introduces a type named Duration (the instance shape) and a value named
Duration (the constructor object). typeof Duration is the constructor’s type.
class Duration {
constructor(public readonly ms: number) {}
}
const d: Duration = new Duration(1000); // 'Duration' as a type
const ctor = Duration; // 'Duration' as a value
type Ctor = typeof Duration; // new (ms: number) => Duration
this types and polymorphic this
Inside a class, this is a type that resolves to the actual subclass at each call site. Returning this from
a builder method keeps the fluent chain typed as the most-derived class.
class QueryBuilder {
protected parts: string[] = [];
where(clause: string): this {
this.parts.push(clause);
return this;
}
}
class SqlQueryBuilder extends QueryBuilder {
limit(n: number): this {
this.parts.push(`LIMIT ${n}`);
return this;
}
}
new SqlQueryBuilder().where("a = 1").limit(10); // still SqlQueryBuilder, so .limit() stays visible
Generic classes and mixins
Classes take type parameters just like functions: class Box<T>. The full treatment — constraints, defaults,
const type parameters, inference — is on Generics.
class Box<T> {
constructor(public value: T) {}
map<U>(f: (value: T) => U): Box<U> {
return new Box(f(this.value));
}
}
new Box(2).map((n) => n.toString()); // Box<string>
The mixin pattern composes reusable slices of behavior with functions that take a class and return an extended subclass, using a generic constructor type as the constraint. See the handbook’s Mixins page.
type Constructor<T = object> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = Date.now();
};
}
class User {
constructor(public name: string) {}
}
const TimestampedUser = Timestamped(User);
const u = new TimestampedUser("Ada");
u.name; // from User
u.createdAt; // from the mixin
For adding behavior through class and member decorators instead, see Decorators and Metadata.
See also
-
Classes — the ES2015 class runtime that TypeScript classes compile to.
-
Generics — type parameters, constraints, and the constructor types mixins depend on.
-
Objects and Interfaces — interfaces,
implementstargets, and structural typing. -
Decorators and Metadata — the standard (TC39) decorator syntax for classes and members.