T
TypeScript: Mejores prácticas y patrones avanzados
5 min de lectura
TypeScriptJavaScriptBest Practices
TypeScript ha revolucionado el desarrollo en JavaScript al agregar tipado estático opcional. En este artículo, exploraremos mejores prácticas y patrones avanzados que te ayudarán a escribir código TypeScript más robusto y mantenible.
1. Configuración estricta
Comienza con una configuración TypeScript estricta:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
2. Tipos vs Interfaces
Cuándo usar Type
// Uniones y tipos primitivos
type Status = 'pending' | 'approved' | 'rejected'
type ID = string | number
// Tipos de utilidad
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// Tuplas
type Coordinate = [number, number]
Cuándo usar Interface
// Objetos y clases
interface User {
id: string
name: string
email: string
}
// Extensión
interface Admin extends User {
permissions: string[]
}
// Implementación
class UserService implements IUserService {
// ...
}
3. Tipos genéricos avanzados
Constraints en genéricos
interface HasLength {
length: number
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length)
return item
}
// Funciona con strings, arrays, etc.
logLength("hello")
logLength([1, 2, 3])
logLength({ length: 10, value: "test" })
Tipos condicionales
type IsArray<T> = T extends any[] ? true : false
type Test1 = IsArray<string[]> // true
type Test2 = IsArray<number> // false
// Tipo de utilidad más complejo
type Flatten<T> = T extends Array<infer U> ? U : T
type Str = Flatten<string[]> // string
type Num = Flatten<number> // number
4. Utility Types personalizados
DeepPartial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
interface Config {
api: {
url: string
timeout: number
headers: {
authorization: string
}
}
}
// Permite actualización parcial profunda
function updateConfig(config: DeepPartial<Config>) {
// ...
}
updateConfig({
api: {
headers: {
authorization: "new-token"
}
}
})
DeepReadonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}
const config: DeepReadonly<Config> = {
api: {
url: "https://api.example.com",
timeout: 5000,
headers: {
authorization: "token"
}
}
}
// Error: Cannot assign to 'authorization' because it is a read-only property
// config.api.headers.authorization = "new-token"
5. Type Guards
User-defined type guards
interface Cat {
type: 'cat'
meow(): void
}
interface Dog {
type: 'dog'
bark(): void
}
type Animal = Cat | Dog
// Type guard
function isCat(animal: Animal): animal is Cat {
return animal.type === 'cat'
}
function makeSound(animal: Animal) {
if (isCat(animal)) {
animal.meow() // TypeScript sabe que es Cat
} else {
animal.bark() // TypeScript sabe que es Dog
}
}
Type guards con in operator
interface Bird {
fly(): void
layEggs(): void
}
interface Fish {
swim(): void
layEggs(): void
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly()
} else {
animal.swim()
}
}
6. Template Literal Types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type APIEndpoint = '/users' | '/posts' | '/comments'
// Genera todas las combinaciones posibles
type APIRoute = `${HTTPMethod} ${APIEndpoint}`
// "GET /users" | "GET /posts" | "GET /comments" | "POST /users" | ...
// Uso práctico
type EventName = 'click' | 'focus' | 'blur'
type EventHandler<T extends EventName> = `on${Capitalize<T>}`
// "onClick" | "onFocus" | "onBlur"
7. Mapped Types avanzados
Key Remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
}
type PersonGetters = Getters<Person>
// {
// getName: () => string
// getAge: () => number
// }
Filtering con Mapped Types
// Filtra solo propiedades string
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
interface User {
id: number
name: string
email: string
isActive: boolean
}
type UserStrings = StringProperties<User>
// { name: string; email: string }
8. Patrón Builder con tipos
class QueryBuilder<T = {}> {
private query: T
constructor(query: T = {} as T) {
this.query = query
}
select<K extends string>(fields: K[]): QueryBuilder<T & { select: K[] }> {
return new QueryBuilder({ ...this.query, select: fields })
}
where<K extends string, V>(
field: K,
value: V
): QueryBuilder<T & { where: { [key in K]: V } }> {
return new QueryBuilder({
...this.query,
where: { ...((this.query as any).where || {}), [field]: value }
})
}
build(): T {
return this.query
}
}
// Uso con inferencia de tipos completa
const query = new QueryBuilder()
.select(['id', 'name'])
.where('age', 25)
.where('city', 'Madrid')
.build()
// Tipo inferido correctamente
9. Discriminated Unions para manejo de estados
type LoadingState = {
status: 'loading'
}
type SuccessState<T> = {
status: 'success'
data: T
}
type ErrorState = {
status: 'error'
error: Error
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState
function handleState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'loading':
return 'Cargando...'
case 'success':
return `Datos: ${JSON.stringify(state.data)}`
case 'error':
return `Error: ${state.error.message}`
}
}
10. Decoradores (experimental)
// Decorador de método para logging
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`Llamando ${propertyKey} con argumentos:`, args)
const result = originalMethod.apply(this, args)
console.log(`${propertyKey} retornó:`, result)
return result
}
return descriptor
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b
}
}
Conclusión
TypeScript es mucho más que agregar tipos a JavaScript. Cuando se usa correctamente, proporciona herramientas poderosas para crear aplicaciones más seguras y mantenibles. Las técnicas presentadas aquí te ayudarán a aprovechar al máximo TypeScript en tus proyectos.
Recuerda que el objetivo no es usar todas estas características, sino elegir las que aporten valor real a tu proyecto y equipo.