TypeScript 24 ๐ท Static Members and Class Expressions
Classes have two sides: the instance side โ properties and methods that live on each object created with new โ and the static side โ properties and methods that live on the class itself. Static members belong to the class, not to any instance. They’re used for factory methods, shared constants, counters, caches, and utility functions that logically belong to the class but don’t depend on a specific instance. Class expressions are the other half of this chapter โ a class defined as an expression rather than a declaration, which can be assigned to a variable, passed around, and used anonymously. Together they round out how classes work in TypeScript.
Key point: A static member lives on the class, not on instances โ ClassName.member, not instance.member. A class expression is a class value that can be assigned, passed, or returned โ useful for factories, decorators, and functional patterns. Static members are shared state or behavior; class expressions are classes as first-class values. Both extend what “class” means beyond the simple declaration.
What a static member is
A static member is declared with the static keyword. It exists on the class itself, not on instances.
class Counter {
static count = 0;
static increment(): void {
Counter.count++;
}
}
Counter.count; // 0
Counter.increment();
Counter.count; // 1
count and increment live on the Counter class. There’s no instance involved. new Counter() isn’t needed.
Static vs instance:
| Member | Access |
|---|---|
| Instance | instance.member |
| Static | ClassName.member |
class User {
name = ''; // instance property
static maxAge = 150; // static property
greet(): string { // instance method
return `Hi, ${this.name}`;
}
static create(name: string): User { // static method
const u = new User();
u.name = name;
return u;
}
}
const u = new User();
u.name; // โ
instance
u.maxAge; // โ not on instance
User.maxAge; // โ
static
User.create('Alice'); // โ
static method
What static members are for:
- Factory methods โ
User.create(),Config.from() - Constants โ
Math.PI,Number.MAX_VALUE - Counters โ how many instances exist
- Caches โ shared across instances
- Utility methods โ belong to the class conceptually, not any instance
- Singletons โ the singleton instance lives as a static field
What static members are not:
- Accessible via instances (
instance.staticMemberis undefined) - Tied to any particular object
- Inherited the same way (they’re inherited, but see below)
Why static members exist: Some things belong to a class conceptually but not to any instance.
Math.PIdoesn’t depend on a Math object.User.create()creates a user but isn’t one. Static members express that โ they’re on the class, usable without an instance, and shared across all instances.
Static properties
A static property is a single value shared by the class.
class Config {
static readonly VERSION = '1.0.0';
static readonly MAX_CONNECTIONS = 100;
static readonly API_URL = 'https://api.example.com';
}
Config.VERSION; // '1.0.0'
Config.MAX_CONNECTIONS; // 100
Constants are the most common static property. static readonly makes them immutable.
Mutable static state:
class Counter {
static count = 0;
constructor() {
Counter.count++;
}
}
new Counter();
new Counter();
Counter.count; // 2
Counter.count tracks how many instances were created. Every new Counter() increments it.
Shared cache:
class UserCache {
private static cache = new Map<number, User>();
static get(id: number): User | undefined {
return this.cache.get(id);
}
static set(id: number, user: User): void {
this.cache.set(id, user);
}
}
The cache is shared across all usages of UserCache. No instance needed.
Lazy static initialization:
class App {
private static instance: App | null = null;
static get(): App {
return App.instance ??= new App();
}
private constructor() {}
}
App.instance is lazily created on first App.get(). The private constructor prevents external new.
Static blocks: For complex initialization, a static { } block runs once when the class is loaded.
class Settings {
static defaults: Record<string, string>;
static {
const env = process.env.NODE_ENV ?? 'dev';
Settings.defaults = env === 'prod'
? { api: 'https://api.example.com' }
: { api: 'http://localhost:3000' };
}
}
Settings.defaults.api; // depends on NODE_ENV
The static block runs once, when the class definition is evaluated. Useful for setup that needs logic.
Why static blocks: Some initialization needs more than a single expression โ reading env vars, parsing config, computing values. A static block runs that logic once, at class load. It’s the class-level equivalent of a constructor.
Why static state matters: Some state belongs to the class, not instances. A cache, a counter, a singleton โ these are shared across everything that uses the class. Static properties make that explicit. Use them carefully: shared mutable state is a source of bugs if not managed.
Static methods
A static method is called on the class, not on an instance.
class MathUtils {
static square(n: number): number {
return n * n;
}
static clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(max, n));
}
}
MathUtils.square(5); // 25
MathUtils.clamp(15, 0, 10); // 10
Factory methods are the classic static method pattern.
class User {
constructor(
public readonly id: string,
public name: string,
public email: string
) {}
static create(name: string, email: string): User {
return new User(crypto.randomUUID(), name, email);
}
static fromJson(json: string): User {
const data = JSON.parse(json);
return new User(data.id, data.name, data.email);
}
}
User.create('Alice', 'alice@example.com');
User.fromJson('{"id":"1","name":"Alice","email":"a@b.c"}');
Factories centralize construction. They can validate, generate defaults, parse input, or return cached instances.
this in static methods: Inside a static method, this refers to the class โ not an instance.
class Base {
static name = 'base';
static getName(): string {
return this.name; // 'base', or subclass's name if called via subclass
}
}
class Sub extends Base {
static name = 'sub'; // โ ๏ธ shadows Base.name
}
Base.getName(); // 'base'
Sub.getName(); // 'sub'
this in a static method is dynamic โ it’s whatever class the method was called on. That lets static methods be inherited and used with subclass context.
Caution: A subclass’s static property with the same name shadows the parent’s. Sub.name replaces Base.name for Sub.getName().
Static methods can’t access instance members:
class User {
name = 'Alice';
static greet(): string {
return `Hi, ${this.name}`; // โ this.name is the class's name, not an instance's
}
}
Static methods don’t have access to instance state. They can create instances and use them, but they can’t reach into this.name expecting instance data.
Utility class pattern:
class Strings {
private constructor() {} // prevent instantiation
static capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
static reverse(s: string): string {
return [...s].reverse().join('');
}
}
Strings.capitalize('hello'); // 'Hello'
new Strings(); // โ private constructor
A class with only static methods and a private constructor is a namespace โ a way to group related functions. TypeScript has namespace for this too, but a class with statics is simpler.
Why static factory methods: They let you control how instances are created.
User.create()can generate an ID, validate input, or return a cached object. A plainnew User()doesn’t have that flexibility. Factories also let you change the constructor later without breaking callers who use the factory.
Static inheritance
Static members are inherited by subclasses, but with a twist โ this in a static method refers to the class the method was called on.
class Animal {
static species = 'unknown';
static describe(): string {
return `Species: ${this.species}`;
}
}
class Dog extends Animal {
static species = 'canine';
}
Animal.describe(); // 'Species: unknown'
Dog.describe(); // 'Species: canine' โ `this` is Dog
Dog inherits describe. When called as Dog.describe(), this is Dog, so this.species is 'canine'. That’s the point โ static methods can be reused with subclass context.
Overriding static methods:
class Base {
static create(): Base {
return new Base();
}
}
class Sub extends Base {
static override create(): Sub {
return new Sub();
}
}
Sub.create() returns a Sub, not a Base. The override keyword works for static methods too, catching typos.
Static this type:
class Base {
static create<T extends typeof Base>(this: T): InstanceType<T> {
return new this() as InstanceType<T>;
}
}
class Sub extends Base {
name = 'sub';
}
const s = Sub.create();
s.name; // 'sub'
The this: T parameter types this as the class being called. InstanceType<T> gives the instance type. This makes Sub.create() return a Sub. Advanced but useful for factory methods in class hierarchies.
Why static inheritance matters: It lets you write generic factory or utility methods on a base class and have them work correctly for subclasses. The this context makes each call resolve to the right class.
Why
thisis dynamic in statics: Static methods are shared code. Ifdescribealways usedAnimal.species, subclasses couldn’t customize it. By usingthis.species, the method reads from whichever class it was called on. That’s polymorphism for the static side.
Class expressions
A class expression is a class defined in an expression position โ assigned to a variable, passed to a function, returned from another function.
const Point = class {
constructor(public x: number, public y: number) {}
};
Point is a variable holding a class. new Point(1, 2) works exactly like a class declaration.
Named class expressions:
const Point = class PointClass {
constructor(public x: number, public y: number) {}
describe(): string {
return `(${this.x}, ${this.y})`;
}
};
const p = new Point(1, 2);
p.describe(); // '(1, 2)'
The inner name (PointClass) is only visible inside the class body โ useful for recursion or self-reference.
Anonymous class expressions:
const createLogger = () => class {
log(msg: string): void {
console.log(msg);
}
};
const Logger = createLogger();
const l = new Logger();
l.log('hello');
The class has no name. That’s fine when the class is only used via the variable.
Passing classes as values:
type Constructor<T> = new (...args: unknown[]) => T;
function instantiate<T>(Ctor: Constructor<T>): T {
return new Ctor();
}
const C = class { name = 'anonymous' };
const instance = instantiate(C);
A class expression can be passed as a value. The Constructor<T> type describes “something newable returning T.”
Returning classes from functions:
function createModel<T>(defaults: T) {
return class {
data: T = { ...defaults };
reset(): void {
this.data = { ...defaults };
}
};
}
const UserModel = createModel({ name: '', email: '' });
const u = new UserModel();
u.data.name = 'Alice';
u.reset();
u.data.name; // ''
The factory returns a class tailored to the defaults. Each call creates a new class. This is a form of metaprogramming โ generating classes on demand.
When to use class expressions:
- Factories that produce classes
- Mixins โ functions that combine classes
- Decorators that replace classes
- Quick, one-off classes assigned to a variable
- Classes with captured state from a closure
Why class expressions exist: Declarations are statements โ they don’t return a value. Expressions do. When you need a class as a value โ to pass, return, or compute โ a class expression is the way. It’s the same class syntax, just in expression position, and it enables factories, mixins, and decorators.
Static typing and typeof
The type of a class is typeof ClassName โ the constructor type.
class User {
constructor(public name: string) {}
greet(): string { return `Hi, ${this.name}`; }
}
type UserConstructor = typeof User;
// { new (name: string): User; prototype: User }
typeof User describes the constructor function โ including new, static members, and the prototype.
InstanceType<T>: Extracts the instance type from a constructor type.
type UserInstance = InstanceType<typeof User>;
// User
Using typeof in functions:
function create<T>(Ctor: new (...args: never[]) => T): T {
return new Ctor();
}
const u = create(User); // u: User
Ctor is a constructor type. The function creates an instance without knowing the specific class.
Static members in typeof:
class Config {
static readonly VERSION = '1.0.0';
static load(): void {}
}
type ConfigCtor = typeof Config;
// includes: new (), VERSION, load
const c: ConfigCtor = Config;
c.VERSION; // '1.0.0'
c.load();
typeof Config includes the static members. A variable typed as typeof Config can access them.
Why typeof matters: It’s how you refer to the class itself as a type โ as opposed to instances of the class. User is the instance type; typeof User is the constructor type. Knowing the difference lets you write generic factories, decorators, and mixins.
Why two types per class: A class produces two things โ the constructor value and the instance type.
typeof Useris the constructor’s type;Useris the instance’s type. TypeScript keeps them separate so you can type variables holding either.
Mixins via class expressions
A mixin is a function that takes a class and returns a new class with added behavior. Class expressions make this possible.
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch(): void {
this.updatedAt = new Date();
}
};
}
function Identified<TBase extends Constructor>(Base: TBase) {
return class extends Base {
readonly id = crypto.randomUUID();
};
}
class Entity {
name = '';
}
const User = Identified(Timestamped(Entity));
const u = new User();
u.name = 'Alice';
u.id; // has id
u.createdAt; // has createdAt
u.touch(); // has touch
Each mixin wraps the base class and adds members. Applying both gives a class with id, createdAt, updatedAt, and touch โ combined without inheritance.
Why mixins: TypeScript only supports single inheritance. Mixins let you compose behavior from multiple sources. Each mixin is a function that returns an enriched class. Combined, they build up a class with many capabilities.
Type inference: The returned class’s type is inferred from the base and the added members. TypeScript tracks what each mixin contributes, so the final class has all the members.
The limitation: TypeScript can’t always infer deeply nested mixin types perfectly, especially with generics. Sometimes you need explicit type annotations to keep the compiler happy.
Why mixins matter: They’re the standard pattern for combining behaviors without multiple inheritance. Need a class that’s
Serializable,Comparable, andTimestamped? Mixins let you compose those traits. The alternative โ a deep hierarchy โ wouldn’t work because inheritance is single.
A full example
A class hierarchy with statics, factories, and a mixin.
// ============================================
// MIXIN
// ============================================
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
readonly createdAt = new Date();
updatedAt = new Date();
touch(): void {
this.updatedAt = new Date();
}
};
}
// ============================================
// BASE CLASS WITH STATICS
// ============================================
class Entity {
static count = 0;
readonly id: string;
name: string;
constructor(name: string) {
this.id = crypto.randomUUID();
this.name = name;
Entity.count++;
}
static reset(): void {
Entity.count = 0;
}
static create<T extends typeof Entity>(
this: T,
name: string
): InstanceType<T> {
return new this(name) as InstanceType<T>;
}
describe(): string {
return `${this.name} (${this.id.slice(0, 8)})`;
}
}
// ============================================
// SUBCLASSES
// ============================================
class User extends Entity {
constructor(name: string, public email: string) {
super(name);
}
override describe(): string {
return `User: ${super.describe()}`;
}
}
class Product extends Entity {
constructor(name: string, public price: number) {
super(name);
}
override describe(): string {
return `Product: ${super.describe()} โ $${this.price}`;
}
}
// ============================================
// WITH MIXIN
// ============================================
const TimestampedUser = Timestamped(User);
// ============================================
// CLASS EXPRESSION
// ============================================
const Admin = class extends User {
constructor(name: string, email: string, public permissions: string[]) {
super(name, email);
}
override describe(): string {
return `Admin: ${this.name} [${this.permissions.join(', ')}]`;
}
};
// ============================================
// USAGE
// ============================================
const u = new User('Alice', 'alice@example.com');
const p = new Product('Keyboard', 79.99);
const a = new Admin('Bob', 'bob@example.com', ['read', 'write']);
console.log(u.describe());
console.log(p.describe());
console.log(a.describe());
console.log(`Total entities: ${Entity.count}`);
// Factory
const created = User.create('Carol', 'carol@example.com');
console.log(created.describe());
// Mixin instance
const tu = new TimestampedUser('Dave', 'dave@example.com');
console.log(tu.describe());
console.log(tu.createdAt);
tu.touch();
// Reset counter
Entity.reset();
console.log(`After reset: ${Entity.count}`);
What this shows:
Entityโ base class with staticcount, staticcreate, and a static method usingthis: Tfor subclass-aware factoriesUser,Productโ subclasses with overridesAdminโ a class expression extendingUserTimestampedโ mixin addingcreatedAtandtouch- Static counter incremented in the constructor
Why this shape: It’s a realistic combination โ statics for shared state and factories, a hierarchy for is-a relationships, a mixin for cross-cutting concerns, and a class expression for on-the-fly extension. All four features in one example.
Complete Example Session
# ============================================
# PART 1: STATIC MEMBERS
# ============================================
cat > statics.ts << 'EOF'
class Counter {
static count = 0;
constructor() {
Counter.count++;
}
static reset(): void {
Counter.count = 0;
}
}
new Counter();
new Counter();
new Counter();
console.log(Counter.count); // 3
Counter.reset();
console.log(Counter.count); // 0
EOF
npx tsc --noEmit statics.ts
# (no errors)
# ============================================
# PART 2: STATIC CONSTANTS
# ============================================
cat > consts.ts << 'EOF'
class Config {
static readonly VERSION = '1.0.0';
static readonly MAX = 100;
}
console.log(Config.VERSION, Config.MAX);
// Config.VERSION = '2.0'; // โ readonly
EOF
npx tsc --noEmit consts.ts
# (no errors)
# ============================================
# PART 3: STATIC FACTORY
# ============================================
cat > factory.ts << 'EOF'
class User {
constructor(
public readonly id: string,
public name: string,
public email: string
) {}
static create(name: string, email: string): User {
return new User(crypto.randomUUID(), name, email);
}
static fromJson(json: string): User {
const d = JSON.parse(json);
return new User(d.id, d.name, d.email);
}
}
const u = User.create('Alice', 'alice@example.com');
console.log(u.name, u.id.slice(0, 8));
EOF
npx tsc --noEmit factory.ts
# (no errors)
# ============================================
# PART 4: STATIC INHERITANCE
# ============================================
cat > inherit.ts << 'EOF'
class Animal {
static species = 'unknown';
static describe(): string {
return `Species: ${this.species}`;
}
}
class Dog extends Animal {
static species = 'canine';
}
console.log(Animal.describe());
console.log(Dog.describe());
EOF
npx tsc --noEmit inherit.ts
# (no errors)
# ============================================
# PART 5: STATIC BLOCK
# ============================================
cat > block.ts << 'EOF'
class Settings {
static defaults: Record<string, string>;
static {
const env = process.env.NODE_ENV ?? 'dev';
Settings.defaults = env === 'prod'
? { api: 'https://api.example.com' }
: { api: 'http://localhost:3000' };
}
}
console.log(Settings.defaults.api);
EOF
npx tsc --noEmit block.ts
# (no errors)
# ============================================
# PART 6: CLASS EXPRESSION
# ============================================
cat > expr.ts << 'EOF'
const Point = class {
constructor(public x: number, public y: number) {}
describe(): string {
return `(${this.x}, ${this.y})`;
}
};
const p = new Point(3, 4);
console.log(p.describe());
// Class as value
type Ctor<T> = new (...args: any[]) => T;
function instantiate<T>(C: Ctor<T>): T { return new C(); }
const Anonymous = class { value = 42 };
console.log(instantiate(Anonymous).value);
EOF
npx tsc --noEmit expr.ts
# (no errors)
# ============================================
# PART 7: MIXIN
# ============================================
cat > mixin.ts << 'EOF'
type Ctor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Ctor>(Base: TBase) {
return class extends Base {
readonly createdAt = new Date();
updatedAt = new Date();
touch(): void {
this.updatedAt = new Date();
}
};
}
class Entity {
name = '';
}
const User = Timestamped(Entity);
const u = new User();
u.name = 'Alice';
console.log(u.name);
console.log(u.createdAt instanceof Date);
u.touch();
EOF
npx tsc --noEmit mixin.ts
# (no errors)
# ============================================
# PART 8: COMPILE AND RUN
# ============================================
npx tsc statics.ts consts.ts factory.ts inherit.ts block.ts expr.ts mixin.ts
node statics.js
# [ 3 ]
# [ 0 ]
node consts.js
# [ 1.0.0 100 ]
node factory.js
# [ Alice <8-char-uuid> ]
node inherit.js
# [ Species: unknown ]
# [ Species: canine ]
node block.js
# [ http://localhost:3000 ]
node expr.js
# [ (3, 4) ]
# [ 42 ]
node mixin.js
# [ Alice ]
# [ true ]
Quick Reference
Static Syntax
| Form | Example |
|---|---|
| Property | static count = 0 |
| Readonly | static readonly MAX = 100 |
| Method | static create(): User { } |
| Private | static #cache = new Map() |
| Block | static { /* init */ } |
Access
| Member | Access |
|---|---|
| Instance | new C().method() |
| Static | C.method() |
| Instance member on class | โ |
| Static member on instance | โ (undefined) |
Static Members
| Type | Use for |
|---|---|
| Property | Constants, counters, caches |
| Method | Factories, utilities |
| Block | Complex initialization |
| Private static | Hidden shared state |
Factory Patterns
| Pattern | Example |
|---|---|
| Create | static create(...): T |
| From JSON | static fromJson(s): T |
| Singleton | static get(): T |
| Builder | static builder(): Builder |
Static Inheritance
| Aspect | Behavior |
|---|---|
| Inherited | โ |
this in static | Refers to called class |
| Subclass override | โ |
| Shadow static property | Replaces parent’s |
Class Expressions
| Form | Example |
|---|---|
| Anonymous | const C = class { } |
| Named | const C = class Named { } |
| Returned | return class { } |
| Passed | fn(class { }) |
typeof and Instances
| Expression | Meaning |
|---|---|
User | Instance type |
typeof User | Constructor type |
InstanceType<typeof User> | Instance type |
new User() | Instance value |
Mixin Pattern
| Step | Code |
|---|---|
| Constructor type | type Ctor<T = {}> = new (...args: any[]) => T |
| Mixin function | function M<TBase extends Ctor>(Base: TBase) { } |
| Returns | return class extends Base { } |
| Apply | const Mixed = M(Base) |
When to Use Static
| Use case | Static |
|---|---|
| Factory | โ |
| Constant | โ |
| Counter | โ |
| Cache | โ |
| Utility | โ |
| Shared state | โ |
| Per-instance data | โ |
| Polymorphic behavior | โ (usually) |
When to Use Class Expressions
| Use case | Expression |
|---|---|
| Factory returning class | โ |
| Mixin | โ |
| Decorator | โ |
| One-off class | โ |
| Simple declaration | โ |
| Named top-level class | โ |
Errors
| Error | Cause |
|---|---|
Property does not exist on type 'typeof C' | Instance member via class |
Property does not exist on type 'C' | Static member via instance |
Class expression is not callable | Missing new |
not assignable to type | Static this context mismatch |
Best Practices
โ Do This:
// Use static for constants
class Config {
static readonly VERSION = '1.0.0';
} // โ
// Use static factories
static create(name: string): User {
return new User(crypto.randomUUID(), name);
} // โ
// Use static blocks for complex init
static {
Settings.defaults = loadDefaults();
} // โ
// Prevent instantiation of utility classes
class Utils {
private constructor() {}
static doThing(): void {}
} // โ
// Use `this: T` in generic factories
static create<T extends typeof Entity>(
this: T, name: string
): InstanceType<T> {
return new this(name) as InstanceType<T>;
} // โ
// Use class expressions for factories
const createModel = <T>(defaults: T) => class {
data = { ...defaults };
}; // โ
// Use mixins for cross-cutting concerns
const Mixed = Timestamped(Identified(Entity)); // โ
// Mark overridden statics with override
static override create(): Sub { return new Sub(); } // โ
โ Don’t Do This:
// Don't use static for per-instance data
class User {
static name = ''; // โ ๏ธ shared across all "instances" // โ ๏ธ
}
// Don't use static methods that need instance state
static greet(): string {
return `Hi, ${this.name}`; // โ this is the class // โ
}
// Don't shadow static properties unknowingly
class Base { static name = 'base'; }
class Sub extends Base { static name = 'sub'; } // โ ๏ธ shadow // โ ๏ธ
// Don't expect static members on instances
instance.staticMethod(); // โ undefined // โ
// Don't forget `new` with class expressions
const C = class {};
C(); // โ must use new // โ
// Don't abuse mixins for simple reuse
const OverMixed = A(B(C(D(E(Base))))); // โ ๏ธ hard to debug // โ ๏ธ
// Don't use static mutable state in concurrent code
static cache = new Map(); // โ ๏ธ shared, no locking // โ ๏ธ
// Don't lose type info with class expressions
const C = class { x = 1 }; // type inference may be limited // โ ๏ธ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Instance access of static | undefined | Use ClassName.member |
| Static access of instance | Not available | Create instance first |
| Shadowing static property | Parent’s value lost | Understand inheritance |
this in static method | Refers to class | Use ClassName or this deliberately |
Missing new on class expression | Runtime error | Always new |
| Mixin type inference | Complex types fail | Add explicit types |
| Shared mutable state | Race conditions | Lock, or avoid |
| Static block timing | Order matters | Runs once at class load |
typeof vs instance | Wrong type used | Know the difference |
| Private constructor + statics | Can’t instantiate | Use factory |
Real-World Examples
1. Static constant
class Config {
static readonly MAX_RETRIES = 3;
}
2. Static counter
class User {
static count = 0;
constructor() { User.count++; }
}
3. Static cache
class Cache {
private static store = new Map<string, unknown>();
static get(k: string) { return this.store.get(k); }
}
4. Static factory
class User {
static create(name: string): User {
return new User(crypto.randomUUID(), name);
}
}
5. Factory from JSON
static fromJson(s: string): User {
const d = JSON.parse(s);
return new User(d.id, d.name);
}
6. Singleton
class App {
private static instance: App | null = null;
static get(): App {
return App.instance ??= new App();
}
private constructor() {}
}
7. Static block
class Config {
static defaults: Record<string, string>;
static {
Config.defaults = loadEnv();
}
}
8. Static utility class
class Strings {
private constructor() {}
static upper(s: string): string { return s.toUpperCase(); }
}
9. Static inheritance
class Base {
static type = 'base';
}
class Sub extends Base {
static type = 'sub';
}
10. Generic static factory
static create<T extends typeof Entity>(
this: T, name: string
): InstanceType<T> {
return new this(name) as InstanceType<T>;
}
11. Class expression โ anonymous
const Point = class {
constructor(public x: number, public y: number) {}
};
12. Class expression โ named
const Point = class PointClass {
constructor(public x: number, public y: number) {}
};
13. Class expression returned
const createModel = <T>(d: T) => class {
data = { ...d };
};
14. Class passed as argument
function make<T>(C: new () => T): T {
return new C();
}
15. typeof for constructor type
type UserCtor = typeof User;
16. InstanceType
type UserInst = InstanceType<typeof User>;
17. Mixin function
type Ctor<T = {}> = new (...args: any[]) => T;
function Identified<T extends Ctor>(B: T) {
return class extends B {
id = crypto.randomUUID();
};
}
18. Applying mixins
const User = Identified(Timestamped(Entity));
19. Mixin with state
function Countable<T extends Ctor>(B: T) {
return class extends B {
static count = 0;
constructor(...args: any[]) {
super(...args);
(this.constructor as any).count++;
}
};
}
20. Static block with env
class Env {
static api: string;
static {
Env.api = process.env.API ?? 'http://localhost';
}
}
Visual: Instance vs Static
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Counter { โ
โ static count = 0; โ
โ value = 0; โ
โ โ
โ static reset() { Counter.count = 0; } โ
โ increment() { this.value++; } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ class side โ instance side
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โ Counter (the class) โ โ c = new Counter() โ
โ โ โ โ
โ Counter.count โ โ c.value โ
โ Counter.reset() โ โ c.increment() โ
โ โ โ โ
โ Shared across all โ โ Per instance โ
โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Static Members Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Config { โ
โ static readonly VERSION = '1.0.0'; โ
โ static load(): void { } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ accessed via class
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Config.VERSION โ '1.0.0' โ
โ Config.load() โ runs โ
โ โ
โ new Config().VERSION โ โ undefined โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Static this in Inheritance
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Animal { โ
โ static species = 'unknown'; โ
โ static describe() { โ
โ return `Species: ${this.species}`; โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ
โ extends
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Dog extends Animal { โ
โ static species = 'canine'; โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Animal.describe() โ this = Animal โ
โ โ 'Species: unknown' โ
โ โ
โ Dog.describe() โ this = Dog โ
โ โ 'Species: canine' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Factory Pattern
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class User { โ
โ constructor( โ
โ public id: string, โ
โ public name: string โ
โ ) {} โ
โ โ
โ static create(name: string): User { โ
โ return new User( โ
โ crypto.randomUUID(), โ
โ name โ
โ ); โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ User.create('Alice')
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ { id: '<uuid>', name: 'Alice' } โ
โ โ
โ Factory generates id, validates, returns โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Class Expression
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Declaration โ
โ โ
โ class Point { } โ
โ โ
โ โ Statement โ
โ โ Not a value โ
โ โ Can't be passed โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Expression โ
โ โ
โ const Point = class { }; โ
โ โ
โ โ Value โ
โ โ Can be passed, returned, assigned โ
โ โ Enables mixins, factories โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Mixin Composition
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Entity { } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Timestamped(Entity)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class extends Entity { โ
โ createdAt = new Date(); โ
โ updatedAt = new Date(); โ
โ touch() { this.updatedAt = new Date(); } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Identified(...)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class extends ... { โ
โ id = crypto.randomUUID(); โ
โ } โ
โ โ
โ Has: createdAt, updatedAt, touch, id โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: typeof vs Instance
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class User { โ
โ static count = 0; โ
โ name = ''; โ
โ greet() { } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ typeof User โ constructor type โ
โ โ
โ { โ
โ new (): User; โ
โ count: number; โ
โ prototype: User; โ
โ } โ
โ โ
โ Includes static members โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ User โ instance type โ
โ โ
โ { โ
โ name: string; โ
โ greet(): void; โ
โ } โ
โ โ
โ Includes instance members โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Static Block Initialization
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Settings { โ
โ static defaults: Record<string, string>; โ
โ โ
โ static { โ
โ // runs once, at class load โ
โ const env = process.env.NODE_ENV; โ
โ Settings.defaults = env === 'prod' โ
โ ? { api: 'https://api.x' } โ
โ : { api: 'http://localhost' }; โ
โ } โ
โ } โ
โ โ
โ Timing: once, when class is defined โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Does the value belong to the class, โ
โ not to any instance? โ
โ โ โ
โ โโโ Yes โโโบ Static member โ
โ โ โ
โ โโโ No โโโบ Instance member โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Do you need the class as a value? โ
โ โ โ
โ โโโ Yes โโโบ Class expression โ
โ โ โ
โ โโโ No โโโบ Class declaration โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Common Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Static members โ where they fit โ
โ โ
โ Constant โ static readonly โ
โ Counter โ static property + inc โ
โ Cache โ static Map โ
โ Factory โ static method โ
โ Singleton โ static + private ctor โ
โ Utility class โ static-only + private ctorโ
โ Init logic โ static { } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Class expressions โ where they fit โ
โ โ
โ Factory โ return class โ
โ Mixin โ function returning class โ
โ Decorator โ wraps class โ
โ Passed value โ fn(class { }) โ
โ Assigned โ const C = class { } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Static member | Lives on the class, not instances |
| Static property | Shared value |
| Static method | Class-level function |
| Static block | Runs once at class load |
| Factory method | Static method returning instances |
| Static inheritance | Subclass inherits and can override |
this in static | Refers to the called class |
| Class expression | Class as a value |
| Mixin | Function combining classes |
typeof Class | Constructor type |
InstanceType<T> | Instance type from constructor |
Key takeaways:
- Static members live on the class โ
ClassName.memberโ not on instances - Static properties are for shared state: constants, counters, caches, singletons
- Static methods are for factories, utilities, and operations that don’t need instance data
- Static blocks run once at class load โ for complex initialization
- Static inheritance works, and
thisin static methods refers to the called class - Class expressions are classes as values โ assignable, passable, returnable
- Factories are the classic use of statics โ
User.create()centralizes construction - Mixins use class expressions to combine behaviors without multiple inheritance
typeof Classis the constructor type;Classis the instance typeInstanceType<T>extracts the instance type from a constructor type- Static methods can’t access instance data โ they have no
thisinstance - Use private constructors with static methods for utility classes and singletons
- Prefer instance members unless the value genuinely belongs to the class
Remember: Static members belong to the class, not instances โ constants, factories, caches, and singletons live there. Class expressions make classes first-class values โ usable in factories, mixins, and decorators. Together they extend what “class” means beyond a simple declaration. Use static when the value is genuinely class-level, use instance members for per-object state, and reach for class expressions when you need to pass, return, or compose classes. That’s the whole toolset โ class declarations for the normal case, and these two features for when the class itself becomes a value or holds shared behavior.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!