TypeScript
Interfaces, clases y factories genéricas
Aplicación de generics en interfaces, clases y factories para modelar colecciones, constructores y APIs que conservan relaciones entre tipos.
- Última actualización
- Actualizada
- Nivel
- Aplicación
TypeScript
Aplicación de generics en interfaces, clases y factories para modelar colecciones, constructores y APIs que conservan relaciones entre tipos.
interface Repository<TEntity> {
findById(id: string): Promise<TEntity | undefined>;
save(entity: TEntity): Promise<void>;
}const productRepository: Repository<Product> = ...;
const userRepository: Repository<User> = ...;Interface completa:
interface Cache<TValue> {
get(key: string): TValue | undefined;
set(key: string, value: TValue): void;
}Cada instancia usa un solo tipo de valor.
Método genérico:
interface Cache {
get<TValue>(
key: string,
parse: (value: unknown) => TValue,
): TValue | undefined;
}Cada llamada puede producir un tipo diferente.
class Collection<TItem> {
#items: TItem[] = [];
add(item: TItem): void {
this.#items.push(item);
}
toArray(): readonly TItem[] {
return [...this.#items];
}
}class Box<T> {
static create(value: T) {}
}No es válido: miembros static pertenecen al constructor compartido, no a una instancia específica de Box<T>.
Haz el método static genérico:
class Box<T> {
private constructor(readonly value: T) {}
static create<TValue>(value: TValue): Box<TValue> {
return new Box(value);
}
}Una clase no declara un constructor con type parameters propios independientes de la clase de la misma forma que una función; usa un método static o factory cuando necesitas inferencia por construcción.
function createRepository<TEntity>(
adapter: StorageAdapter<TEntity>,
): Repository<TEntity> {
return {
findById: (id) => adapter.findById(id),
save: (entity) => adapter.save(entity),
};
}El tipo se infiere desde el adapter.
type Constructor<TInstance, TArgs extends unknown[]> =
new (...args: TArgs) => TInstance;
function instantiate<TInstance, TArgs extends unknown[]>(
ConstructorValue: Constructor<TInstance, TArgs>,
...args: TArgs
): TInstance {
return new ConstructorValue(...args);
}class ApiClient<TError = Error> {
// ...
}abstract class BaseRepository<
TEntity extends { id: string },
> {
abstract findById(
id: string,
): Promise<TEntity | undefined>;
}class ProductRepository extends BaseRepository<Product> {
// TEntity queda fijado como Product
}class CachedRepository<
TEntity extends { id: string },
> extends BaseRepository<TEntity> {
// ...
}type Result<TValue, TError = Error> =
| { ok: true; value: TValue }
| { ok: false; error: TError };type EventMap = Record<string, unknown>;
class EventEmitter<TEvents extends EventMap> {
on<TKey extends keyof TEvents>(
event: TKey,
handler: (payload: TEvents[TKey]) => void,
): void {
// ...
}
}emitter.on("product.created", (event) => {
event.product;
});class QueryBuilder<TResult> {
select<TSelected>(
project: (value: TResult) => TSelected,
): QueryBuilder<TSelected> {
// runtime implementation omitted
return new QueryBuilder<TSelected>();
}
}Cada etapa transforma el type parameter.
Un parameter puede no aparecer como propiedad runtime, pero separar estados estáticos.
type Open = { readonly state: "open" };
type Closed = { readonly state: "closed" };
class Connection<TState> {
private declare readonly __state: TState;
}El campo declare no emite JavaScript. Este patrón puede modelar protocolos, pero aumenta complejidad y puede evitarse con APIs separadas.
interface FormField<TValue> {
value: TValue;
validate(value: TValue): string[];
serialize(value: TValue): string;
}
const priceField: FormField<number> = {
value: 0,
validate: (value) => value >= 0 ? [] : ["Invalid price"],
serialize: String,
};any[] y perder parámetros.¿Por qué un método static no puede usar directamente el T de class Box<T>?
Porque el miembro static pertenece al único constructor Box compartido, mientras T se elige para cada tipo de instancia como Box<string> o Box<number>.
Mapped types transforma propiedades de un object type de forma programática.