DEV Community

Pedro Uzcátegui
Pedro Uzcátegui

Posted on

Typescript Interfaces and Generics.

If you're like me and are used to functional programming and don't touch OOP, or do it very rarely, then this tutorial is for you.

Objects in Typescript

Let's suppose that a function receives an object, and you want to write some types for the parameters.

function getFirstName(person: {name: string, lastname: string}){
  return person.name;
}
Enter fullscreen mode Exit fullscreen mode

Now, this is useful in this function, but what if we want to duplicate this object into a different function?

// Bad example
function getFirstName(person: {name: string, lastname: string}){
  return person.name;
}

function getLastName(person: {name: string, lastname: string}){
  return person.lastname;
}
Enter fullscreen mode Exit fullscreen mode

This is effective, but is not efficient, because this would mean that if we want to emulate or replicate this literal object type, we would need to define again the same object into every parameter of the functions.

Which would be the most efficient way to replicate that type? We could use types:

type Person = {
  name: string,
  lastname: string,
}

function getFirstName(person: Person){
  return person.name
}
Enter fullscreen mode Exit fullscreen mode

Or we could use interfaces:

interface Person {
  name: string,
  lastname: string,
}

function getFirstName(person: Person){
  return person.name
}
Enter fullscreen mode Exit fullscreen mode

Typescript Interfaces

The examples above are great, but when we need to use one or another? Well, let's suppose you have an address and an extended address types.

type Address = {
  city: string,
  state: string,
  country: string,
  postalCode: number
}

type ExtendedAddress = {
  street: string,
  houseNumber: number,
  region: string,
  city: string, // The rest of the object from here is the same as the type Address 
  state: string,
  country: string,
  postalCode: number
}
Enter fullscreen mode Exit fullscreen mode

We know are looking at some duplicated lines of code. We could save some space if we implement another mechanism that allows us to inherit all of the properties in the Address type and allow us to define new properties besides those ones.

We are looking to extend an interface!

interface Address {
  city: string,
  state: string,
  country: string,
  postalCode: number
}

interface ExtendedAddress extends Address {
  street: string,
  houseNumber: number,
  region: string
}
Enter fullscreen mode Exit fullscreen mode

Now, extended address contains all of the properties inside the Address interface!

Extending from multiple interfaces.

Also, another neat trick is that we can as well extend from multiple interfaces!

interface Colorful {
  color: string
}

interface Circle {
  radius: number
}

interface ColorfulCircle extends Colorful, Circle {} // The curly braces at the end means that we're not passing any properties to the interfaces.

const cc: ColorfulCircle = {
  color: "red"
  radius: 42
}
Enter fullscreen mode Exit fullscreen mode

Intersection Types

We can achieve the same behavior as above performing an Intersection Type

interface Colorful {
  color: string
}

interface Circle {
  radius: number
}

type ColorfulCircle = Colorful & Circle;
Enter fullscreen mode Exit fullscreen mode

We can also pass the intersection type directly to the parameters in the function if we believe that is not necessary to produce a new intersection type

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}

// okay
draw({ color: "blue", radius: 42 });

// oops, misspelled "radius", this will prompt an error.
draw({ color: "red", raidus: 42 });
Enter fullscreen mode Exit fullscreen mode

Generics in typescript

When we want to reuse a type or an interface using different data types, we might use generics to describe this behavior.

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}

const box: Box<string>;
setContents(box, "Hello");
box.contents // "Hello";
Enter fullscreen mode Exit fullscreen mode

We can reuse this easily because we can specify the Type when we call the function.

Nested Generics

One of the most essential things to work in real life projects, is to understand these nested generics. Those could be seen as a pain in the ass to understand (and in some occasions, they are).

But is really not that hard if we know the types we're receiving from the parent types.

type OrNull<Type> = Type | null;

type OneOrMany<Type> = Type | Type[];

// In this next implementation, we're using the OneOrMany generic, that can be one or many of the specified type.

// Here, we have the OneOrManyOrNull type, this could mean that we could expect to have (Type | Type[]) | null

// You could go an replace the types to understand them better!

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

type OneOrManyOrNull<Type> = OneOrMany<Type> | null

type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
Enter fullscreen mode Exit fullscreen mode
Array Generic

Typescript allows us to work with some of the pre-defined generics that can be helpful to specify certain behaviors with certain data types, like arrays.

This is the formal definition of the Array generic:

interface Array<Type> {
  /**
   * Gets or sets the length of the array.
   */
  length: number;

  /**
   * Removes the last element from an array and returns it.
   */
  pop(): Type | undefined;

  /**
   * Appends new elements to an array, and returns the new length of the array.
   */
  push(...items: Type[]): number;

  // ...
}
Enter fullscreen mode Exit fullscreen mode

An example of this could be like:

function doSomething(value: Array<string>) {
  // ...
}

let myArray: string[] = ["hello", "world"];

// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
Enter fullscreen mode Exit fullscreen mode

We also have some other generics like Map<>, Set<> and Promise<>.

We also have an special type of array generic, the ReadonlyArray!

function doStuff(values: ReadonlyArray<string>) {
  // We can read from 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);

  // ...but we can't mutate 'values'.
  values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.
}
Enter fullscreen mode Exit fullscreen mode

We can assign values to a readonly array in its declaration, but we can't modify it or use it as a value

// This is correct.
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

// This is not correct, ReadonlyArray is intended to be used as a type, not as a value
const someValue = new ReadonlyArray();
Enter fullscreen mode Exit fullscreen mode

If we don't want to use generics, we can use readonly Type[]

function doStuff(values: readonly string[]) {
  // We can read from 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);

  // ...but we can't mutate 'values'.
  values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.
}
Enter fullscreen mode Exit fullscreen mode

Tuple Types

You can create tuple types by specifying it like this:

type StringNumberPair = [string, number]
// It doesn't have to be only 2 values, can be more if needed.

function doSomething(pair: StringNumberPair) {
  const a = pair[0];
  const b = pair[1];
  const c = pair[2] // Error, cannot access index 2 on StringNumberPair
}

doSomething(["hello", 42]);
Enter fullscreen mode Exit fullscreen mode

You can also destructure a tuple using destructuring in javascript.

type Point = [number, number]

function doSomething(point: Point){
  let [x,y] = point;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)