Herencia, override y composición en TypeScript | Nicolás Garzón
TypeScript
Copiar class Employee {
constructor ( readonly name: string ) { }
describe ( ) : string {
return this . name;
}
}
class Manager extends Employee {
constructor (
name: string ,
readonly teamSize: number ,
) {
super ( name) ;
}
} Una subclase debe llamar super antes de usar this cuando define constructor.
TypeScript
Copiar class Manager extends Employee {
override describe ( ) : string {
return ` ${ this . name} manages ${ this . teamSize} ` ;
}
} Con noImplicitOverride, el modificador es obligatorio cuando reemplazas un miembro.
Si la base renombra o elimina el método, la subclase deja de compilar.
El miembro de la subclase debe ser compatible con el contrato base.
TypeScript
Copiar class Base {
process ( value: Animal) : Animal { }
}
class Derived extends Base {
override process ( value: Animal) : Dog { }
} Un retorno más específico puede ser compatible. Restringir parámetros puede ser inseguro.
TypeScript
Copiar class Employee {
protected formatName ( ) : string {
return this . name. toUpperCase ( ) ;
}
} Permite reutilización en subclases, pero vuelve esos detalles parte del contrato de herencia.
TypeScript
Copiar class Base {
name = "base" ;
}
class Derived extends Base {
name = "derived" ;
} El field de subclase se inicializa según reglas runtime después de super, pudiendo reemplazar el valor base.
TypeScript
Copiar class AnimalHouse {
resident: Animal;
}
class DogHouse extends AnimalHouse {
declare resident: Dog;
} declare cambia el tipo sin emitir una nueva inicialización que sobrescriba el valor de la base. Debe existir una garantía runtime real.
TypeScript
Copiar class Base {
constructor ( ) {
this . initialize ( ) ;
}
protected initialize ( ) : void { }
} Una subclase puede override initialize, pero sus fields aún no están inicializados cuando el constructor base lo llama. Evita este patrón.
Una subclase debe poder usarse donde se espera la base sin romper expectativas.
TypeScript
Copiar class ReadOnlyRepository extends Repository {
override save ( ) : never {
throw new Error ( "Not supported" ) ;
}
} Aunque compile bajo alguna firma, viola el contrato esperado. Separa capacidades:
TypeScript
Copiar interface Reader< T > { }
interface Writer< T > { } TypeScript
Copiar class CachedRepository extends ApiRepository { } TypeScript
Copiar class CachedRepository implements ProductRepository {
constructor (
private readonly source: ProductRepository,
private readonly cache: ProductCache,
) { }
} La segunda puede envolver cualquier implementación y hace dependencias explícitas.
TypeScript
Copiar class LoggingRepository implements ProductRepository {
constructor (
private readonly inner: ProductRepository,
private readonly logger: Logger,
) { }
async save ( product: Product) : Promise < void > {
this . logger. info ( "Saving product" , { id: product. id } ) ;
return this . inner. save ( product) ;
}
} TypeScript
Copiar abstract class Serializer< T > {
abstract serialize ( value: T ) : string ;
abstract parse ( value: string ) : T ;
} TypeScript no tiene un modificador final general. Puedes:
Usar constructor private y factories.
No exportar la clase.
Preferir factories o objetos.
Diseñar miembros privados que dificulten extensión.
Pero un consumidor con acceso al constructor runtime puede extenderla si no existe una barrera real.
TypeScript
Copiar type DiscountPolicy = (
order: Order,
customer: Customer,
) => Cents;
class CheckoutService {
constructor (
private readonly discountPolicy: DiscountPolicy,
) { }
} La composición evita una jerarquía por cada estrategia.
Extender para reutilizar código sin relación de sustitución.
Omitir override en proyectos con noImplicitOverride.
Hacer más específico un parámetro y romper consumidores.
Usar protected como acceso público para subclases.
Llamar métodos sobreescribibles en el constructor base.
Sobrescribir fields sin entender orden de inicialización.
Crear subclases que lanzan “not supported”.
Esperar un modificador final inexistente.
extends usa el modelo runtime de clases de JavaScript.
override verifica que reemplazas un miembro real.
La firma derivada debe respetar el contrato base.
Protected expone detalles a la jerarquía.
Declare puede refinar sin emitir un field nuevo.
La composición suele reducir acoplamiento.
El checker no demuestra por sí solo el principio de sustitución.
¿Por qué una subclase que reemplaza save por un método que siempre lanza puede ser un mal diseño aunque compile?
Respuesta Porque deja de poder sustituir a la clase base según su contrato observable: los consumidores esperan que guardar sea una operación soportada.
Static, private constructors y factories modela creación controlada y miembros del lado constructor.