Typescript avanzado: Tips & Tricks

Typescript avanzado: Tips & Tricks

Si has llegado hasta aquí es por que ya sabes que usar Typescript en tus proyectos es una buena idea y ya pasaste el punto inicial de fricción de agregar tipos en tu código Javascript, ahora: ¿Qué más puedes hacer?

El uso de Tipos en tu código te permite expresar diferentes restricciones y requerimientos en la forma de tus datos. Muchos de estos casos de uso pueden ser considerados avanzados y también patrones que encontrarás constantemente, por ejemplo:

Una cuenta de usuario que debe tener "username" o "email", y "un avatar" o "un emoji"

Es decir, tienes un tipo de dato con una restricción: Cuando un atributo está presente algún otro atributo no debe estarlo.

Este tipo de dato o patrón se conoce como "Discriminated Unions".

Discriminated Unions

Hace un tiempo hice un hilo sobre esto mismo

¿Cómo expresas estas condiciones con @typescript?

La solución es una combinación de distintos tipos de datos y el uso de atributos opcionales y never tal como en el siguiente ejemplo (Link al playground de typescript)

type WithUsername = {
    username: string;
    email?: never
}

type WithEmail = {
    username?: never;
    email: string;
}

type WithAvatar = {
    emoji?: never;
    avatar: string
}

type WithEmoji = {
    emoji: string;
    avatar?: never
}



type User = { id: number } & (WithUsername | WithEmail) & (WithAvatar | WithEmoji)

const userWithNameAndAvatar: User = { id: 1, username: 'username', avatar: 'avatar' }

const userWithEmailAndAvatar: User = { id: 2, email: 'email', avatar: 'avatar'}

const userWithNameAndEmoji: User = { id: 3, username: 'username', emoji: 'emoji' }

const userWithEmailAndEmoji: User = { id: 4, email: 'email', emoji: 'emoji'}

const wrongUser: User = { id:5, username: 'username', email: 'email', emoji: 'emoji'} // Error

const wrongUser2: User = { id:5, username: 'username', emoji: 'emoji', avatar: 'avatar' } // Error

Puedes ver que se crearon distintos tipos y luego un tipo que une todos los demás, la combinación de opcional y never permite definir que un atributo este presente o dependiendo del otro atributo del conjunto.

Cuando creas tipos de datos complejos te encontrarás con ciertos patrones que fácilmente se pueden extraer como utilidades, y el equipo de Typescript lo sabe y ofrece variadas utilidades listas para usar, revisemos algunas

Partial:

Esta utilidad te permite construir un tipo de datos donde todas sus propiedades son opcionales, creando así un tipo que en efecto es un "sub-tipo" del tipo original.

React.Context es un caso de uso comun en donde el valor inicial del objeto context es desconocido, ergo, opcional.

type Data = {
    title: string;
    description: string;
    amount: number;
}

type Context = Partial<Data>

const c1: Context = {}
const c2: Context = { title: 'Title' }

function updateContext(data: Context, attrToUpdate: Context) {
    return {...data, ...attrToUpdate}
}

updateContext(c1, { amount: 10 }) // { amount: 10}

Required:

Al contrario de Partial, Required indica que todas las propiedades del tipo pasado como "argumento" son requeridas.

type Data = {
    title?: string;
    description?: string;
    amount?: number;
}

type AllRequired = Required<Data>

const obj1: Data = {}

const obj2: AlLRequired = { title: 'Title'} // Error!

ReadOnly:

Te permite crear un tipo en donde todas las propiedades serán de solo lectura, es decir no se pueden modificar generando el mismo efecto que congelar un objeto con Object.freeze pero en tiempo de "compilación" permitiendote encontrar erores antes de llegar al navegador

type Data = {
    title: string;
    description: string;
    amount?: number;
}

const obj: ReadOnly<Data> = {
    title: 'Titulo',
    description: 'description'
}

const obj.title = 'Otro titulo'; // Error, title es de solo lectura