Classes give you runtime structure; interfaces give you compile-time contracts. Abstract classes sit between the two: they can carry shared implementation and force subclasses to fill in the missing pieces. This lesson shows how abstract, implements and override work together, when to choose an interface over an abstract class, and how to type classes themselves as values.
An abstract class cannot be instantiated directly. It may declare abstract members (methods or properties with no implementation) that every concrete subclass must provide, alongside normal members with real code:
abstract class Repository<T extends { id: string }> {
protected items = new Map<string, T>();
abstract validate(item: T): void; // no body allowed
abstract readonly entityName: string; // abstract property
save(item: T): T {
this.validate(item); // calls the subclass implementation
this.items.set(item.id, item);
return item;
}
}
class UserRepository extends Repository<{ id: string; email: string }> {
readonly entityName = "user";
validate(item: { id: string; email: string }) {
if (!item.email.includes("@")) throw new Error("invalid email");
}
}save is the template method pattern: the base class defines the algorithm and delegates one step to subclasses. A subclass that forgets validate fails to compile with "Non-abstract class 'UserRepository' does not implement inherited abstract member".
implements checks that a class satisfies an interface. It adds no code and does not copy anything into the class; it is purely a contract check:
interface Comparable<T> { compareTo(other: T): number }
interface Printable { print(): string }
class Version implements Comparable<Version>, Printable {
constructor(private parts: number[]) {}
compareTo(other: Version): number {
for (let i = 0; i < 3; i++) {
const diff = (this.parts[i] ?? 0) - (other.parts[i] ?? 0);
if (diff !== 0) return diff;
}
return 0;
}
print() { return this.parts.join("."); }
}A class may implement several interfaces but extend only one class. Because TypeScript is structural, a class with the right members satisfies the interface even without implements; the keyword makes intent explicit and reports errors at the class rather than at the usage site. implements does not infer method parameter types, so annotate them as usual.
override and noImplicitOverrideThe override modifier states that a method replaces a base-class member. With noImplicitOverride enabled in tsconfig.json, TypeScript reports two mistakes that are otherwise silent: overriding without the keyword, and marking a method override when the base class has no such member (typically a typo after a rename):
class Base { greet() { return "hi"; } }
class Child extends Base {
override greet() { return "hello"; } // OK
override gret() { return "x"; } // Error: no member 'gret' in base class
}| Question | Interface | Abstract class |
|---|---|---|
| Can hold implementation? | No | Yes |
| Exists at runtime? | No (erased) | Yes (real class, instanceof works) |
| Multiple per class? | Implement many | Extend one |
| Can describe plain objects and functions? | Yes | No |
| Constructor parameters, protected state? | No | Yes |
Use an interface when you only need a shape, when plain objects must qualify, or when you want to decouple modules. Use an abstract class when several subclasses share real code or state and you want instanceof checks. Many designs use both: an abstract base class that implements a public interface, as in the sample at the top.
A class declaration creates both a type (its instances) and a value (the constructor). To accept a class itself, for example in a factory, type the constructor with a new signature:
interface Plugin { run(): void }
function instantiate<T extends Plugin>(Ctor: new () => T): T {
return new Ctor();
}
class Logger implements Plugin { run() { console.log("logging"); } }
instantiate(Logger).run();
type Instance = InstanceType<typeof Logger>; // Loggertypeof Logger is the constructor type; Logger alone is the instance type. Abstract classes do not satisfy new () => T; use abstract new () => T to accept them.
protected over private for state that subclasses legitimately need.noImplicitOverride; it costs nothing and prevents silent breakage during refactors.What happens when you write `new Shape()` for an abstract class `Shape`?
abstract class cannot be instantiated and may declare abstract members that subclasses must implement.implements verifies that a class matches an interface; it adds no runtime behaviour.override plus noImplicitOverride catches accidental and phantom overrides.new () => T and typeof Class to type constructors.Next lesson: Generics — write functions, classes and types that work for many types while staying fully type-safe.