10 tips avanzados de TypeScript que mejorarán tu código
Técnicas avanzadas de TypeScript que usamos a diario: utility types, inferencia, guards y patrones que harán tu código más seguro y mantenible.
TypeScript es mucho más que añadir : string a tus variables. Estos son los tips avanzados que usamos en Fluxer Labs para escribir código más seguro y expresivo.
1. Const assertions para literales
// Sin as const
const config = {
endpoint: '/api/users',
method: 'GET'
};
// tipo: { endpoint: string, method: string }
// Con as const
const config = {
endpoint: '/api/users',
method: 'GET'
} as const;
// tipo: { readonly endpoint: '/api/users', readonly method: 'GET' }
Útil para configuraciones que no deben cambiar.
2. Discriminated unions para estados
// ❌ Mal: campos opcionales confusos
type ApiResponse = {
data?: User;
error?: string;
loading?: boolean;
};
// ✅ Bien: estados claros y mutuamente excluyentes
type ApiResponse =
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: string };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'loading':
return <Spinner />;
case 'success':
return <UserCard user={response.data} />; // data existe seguro
case 'error':
return <Error message={response.error} />; // error existe seguro
}
}
3. Template literal types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiVersion = 'v1' | 'v2';
// Genera todas las combinaciones automáticamente
type ApiEndpoint = `/${ApiVersion}/${string}`;
type RequestKey = `${HttpMethod}:${ApiEndpoint}`;
// 'GET:/v1/users', 'POST:/v2/products', etc.
const cache: Record<RequestKey, unknown> = {};
4. Satisfies para validar sin perder inferencia
type Colors = Record<string, [number, number, number]>;
// Con type annotation: perdemos los keys específicos
const colors: Colors = {
red: [255, 0, 0],
green: [0, 255, 0],
};
colors.red; // OK
colors.blue; // OK (pero no existe!) - TypeScript no sabe qué keys hay
// Con satisfies: validamos Y mantenemos inferencia
const colors = {
red: [255, 0, 0],
green: [0, 255, 0],
} satisfies Colors;
colors.red; // OK
colors.blue; // Error! Property 'blue' does not exist
5. Type guards personalizados
type Fish = { swim: () => void };
type Bird = { fly: () => void };
// Type guard con 'is'
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript sabe que es Fish
} else {
pet.fly(); // TypeScript sabe que es Bird
}
}
6. Infer en conditional types
// Extraer tipo de retorno de una función
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Extraer tipo de elementos de un array
type ArrayElement<T> = T extends (infer E)[] ? E : never;
// Extraer props de un componente React
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
// Uso
type UserProps = PropsOf<typeof UserCard>; // { name: string; age: number }
7. Mapped types con modificadores
type User = {
id: number;
name: string;
email: string;
};
// Hacer todas las propiedades opcionales
type PartialUser = Partial<User>;
// Hacer todas las propiedades requeridas
type RequiredUser = Required<PartialUser>;
// Hacer todas las propiedades readonly
type ReadonlyUser = Readonly<User>;
// Crear un tipo solo con algunas keys
type UserCredentials = Pick<User, 'email'>;
// Crear un tipo sin algunas keys
type PublicUser = Omit<User, 'email'>;
// Customizado: hacer solo algunas propiedades opcionales
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UserWithOptionalEmail = PartialBy<User, 'email'>;
8. Exhaustive checks con never
type Status = 'pending' | 'approved' | 'rejected';
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Esperando...';
case 'approved':
return 'Aprobado!';
case 'rejected':
return 'Rechazado';
default:
// Si añades un nuevo status y olvidas manejarlo,
// TypeScript dará error aquí
const _exhaustive: never = status;
return _exhaustive;
}
}
9. Branded types para IDs
// Evita mezclar IDs de diferentes entidades
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = createUserId('user-123');
const postId = createPostId('post-456');
getUser(userId); // OK
getUser(postId); // Error! No puedes pasar PostId donde se espera UserId
10. Utility type personalizado: DeepPartial
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type Config = {
server: {
port: number;
host: string;
ssl: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
};
};
// Todas las propiedades anidadas son opcionales
type PartialConfig = DeepPartial<Config>;
const config: PartialConfig = {
server: {
port: 3000,
// host y ssl son opcionales
},
// database es opcional
};
Bonus: Configuración recomendada de tsconfig
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
Conclusión
TypeScript es una herramienta poderosa cuando aprovechas todo su potencial. Estos patrones no son solo para presumir en code reviews - realmente previenen bugs y hacen el código más mantenible.
Empieza incorporando uno o dos de estos tips en tu próximo proyecto y ve añadiendo más conforme te sientas cómodo.
¿Quieres que tu equipo domine TypeScript? En Fluxer Labs ofrecemos formación y consultoría. Contáctanos.