Types vs Interfaces ¿Cuándo utilizar cada una?

Types vs Interfaces ¿Cuándo utilizar cada una?

Una pregunta común cuando comienzas a escribir Typescript es: ¿Debo usar Types o Interfaces, cuál es la diferencia?, en este artículo te ayudaré a aclarar a decidir que utilizar y por qué.

En Typescript puedes definir el tipo de un dato o una función escribiendo su definición directamente en donde declaras dicho trozo de código, como en el siguiente ejemplo:

function get3DCoord(point: { x: number, y:number}):
  {x: number, y: number, z:number} 
{
  // Hacer algún calculo
  return coord3d
}

Pero, dicha metodología o sintaxis puede volverse confusa rápidamente. Además, ¿qué ocurre si quieres utilizar el mismo tipo retornado por esta función en diferentes partes? Aquí es donde entras los alias de tipo Type Aliases

Type Alias

Esta es una forma conveniente de hacer referencia a un tipo más de una vez, como si de una variable se tratase. Un alias de tipo es exactamente lo que su nombre indica: Un nombre para un tipo, utilizando el mismo ejemplo anterior:

type Point = {
  x: number;
  y: number;
}

type Point3D = {
  x: number;
  y: number;
  z: number;
}
function get3DCoord(point: Point): Point3D {
  // Hacer algún calculo
  return coord3d
}

Lo que claramente permite una mejor legibilidad y mantenibilidad del código. Pero no solo se "ve mejor", si no que también permite dar "nombre" a toda forma de tipado, no solo tipos objetos, puedes utilizar uniones, intersecciones y utilidades, incluso hacer referencia a otros tipos que hayas creado.

Importante notar que estos alias son sólo alias, es decir, este alias siempre hará referencia a su contenido, no crea un nuevo tipo de dato.

Interfaces

Otra forma de declarar o definir un nombre para un tipo objeto es utilizar la palabra clave interface.

interface Point {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}
function get3DCoord(point: Point): Point3D {
  // Hacer algún calculo
  return coord3d
}

Como vez, el cambio es sutil y el funcionamiento (hasta este punto) es el mismo que anteriormente.

¿Entonces como decides que utilizar en tu código?

Types vs Interfaces

Lo primero que revisaremos son sus similitudes. Son tan parecidos entre sí que la mayoría de las veces puedes usar ambas formas de manera intercambiable. Casi todas las características de una interface estan disponibles al usar type, pero con una importante diferencia que revisaremos más adelante.

Types e interfaces pueden ser utilizados para describes objetos o funciones.
/* Type Alias */

type SomeData = {
  id: number | string;
  userName: string;
  location: LocationType // Este es otro alias
}

type SomeFunction =  (data: SomeData) => void;


/* Interface  */

interface SomeData {
  id: number | string;
  userName: string;
  location: LocationType;
}

interface SomeFunction {
  (data: SomeData): void;
}

En este ejemplo se utiliza la palabra clave typepara crear un alias a un objeto y otro alias para definir el tipode una función. Pero lo mismo puede realizarse utilizando interface. Ambas versiones pueden ser reutilizadas y exportadas.

Puedes extender types e interfaces.

Extender un tipo es básicamente agregar más características o atributos, en el caso de un alias utilizando type esto se logra al utilizar la intersección de tipos &. En el caso de una interfacela extención se realiza utilizando extends (muy similar a extender una clase).

/* Type Alias */

type SomeData = {
  id: number | string;
  userName: string;
  location: LocationType // Este es otro alias
}

type GlobalData = SomeData & {
  phone: number;
}


/* Interface  */

interface SomeData {
  id: number | string;
  userName: string;
  location: LocationType;
}

interface GlobaData extends SomeData {
  phone: number;
}

Además es posible "mezclar" ambas formas. Una interface puede extender un type y viceversa, un type puede extender una interface utilizando el operador intersección &


type SomeData = {
  id: number | string;
  userName: string;
  location: LocationType // Este es otro alias
}


interface GlobaData extends SomeData {
  phone: number;
}


/**************************/


interface SomeData {
  id: number | string;
  userName: string;
  location: LocationType // Este es otro alias
}


type GlobaData = SomeData & {
  phone: number;
}

Y aquí acaban lñas similitudes y comienzan las diferencias, diferencias que se hacen notar en usos más avanzados de Typescript y que no todos los desarrolladores encontrarán necesarias, por lo tanto la decisión de si usar type o interface queda más a cargo de tu gusto o de la decisión del equipo.

A modo general, puedes usar interface hasta que te sea necesario utilizar características de type.

Sólo type puede crear alias para tipos primitivos, uniones o tuplas:
type MiNumero = number;

type StringOrArray = string | any[]

type Tupla = [string, number]
Sólo interfaceposee la característica de "mezclado de declaraciones" (Declaration Merging).

Esta característica es cuando el compilador de Typescript mezcla dos o más tipos en una sola declaración basado en que dichas deficiones comparten el mismo nombre dentro del módulo.

interface Dog {
  name: string;
}

interface Dog {
  breed: string;
}

interface Dog {
  owner: Person
}

class MyDog implements Dog {
  name = "Rocky"
  breed = "unknown";
  owner = {
    name: "Me"
  }
}

const myDog = new MyDog();
console.log(myDog) // {name: "Rocky", breed: "unknown", owner: { name: "Me"}}

Dado que cada una de las interfaces utilizadas en el ejemplo tienen el mismo nombre, Typescript las reconocerá y utilizará como si de una sola interfaz se tratase.

Ahora bien, si alguna de las propiedades de las interfaces que se uniran comparte el mismo nombre pero diferente tipo, Typescript emitirá un error. Pero, si dicha propiedad es una función, entonces Typescript unirá esta propiedad creando una sobrecarga de funciones.

interface Dog {
  bark(volume: number);
}

interface Dog {
  bark(type: 'low' | 'high');
}

const dog: Dog = {
  bark: (volumeOrType) => volumeOrType
}

console.log(dog.bark(100)) // bark(volume: number) es utilizada
console.log(dog.bark("low")) // bark(type: 'low' | 'high') es utilizada

Importante: Al mezclar interfaces que contengan funciones de igual definición, la función en la última interfaz declarada aparecer como función inicial en la interfaz mezclada, es decir, la última interfaz declarada tiene precedencia.

Ahora, ¿para que puede ser útil esta característica de las interfaces en Typescript?

Esta funcionalidad es muy utilizada por autores de librerías para proveer a sus usuarios formas de extender las definiciones de tipos de una manera segura, por ejemplo, revisa el siguiente código.

/* Esta es MyAwesomeLibrary */
export interface WithReturn<Data> {
  data: Promise<Data>
  returnData: Record<string, unknown>
}

/* Ahora el código del usuario de la librería */

import { WithReturn } from 'awesone-library'

declare module 'awesome-library' {
  export interface WithReturn {
    foo: { extra: string }
  } 
}


// En otra parte de tu aplicación

import { WithReturn } from 'awesome-library'

const returnData: WithReturn<typeof data> 
/*
  returnData = { 
    data: Promise<typeof data>,
    returnData: Record<string, unknown>,
    foo: { extra: string
    }
 */

En conclusión

¿Cuándo usar interface o type?

Por lo general verás el uso de interface en la construcción de librerías, en otras palabras si lo que estás desarrollando es una aplicación, lo más probable es que puedas usar cualquiera de las dos formas de definr un tipo, pero:

Usa type cuando:

  • Necesitas definir alias para tipos primitivos.
  • Necesitas definir una tupla.
  • Necesitas definir el tipo de una función.
  • Necesitas deifnir uniones, uniones disjuntas, condicionales, mapped types, etc.

Usa interface cuando:

  • Cuando quieres utilizar el mecanismo de "mezcla de declaraciones" (declaration mergin)
  • Cuando no existe ninguna necesidad de usar type al crear un tipo para un objeto.