DEV Community

loading...

Algebraic Data Type(ADT) with TypeScript

mikeskoe profile image Mike Skoe ・2 min read

ADT in OCaml

Functional programming languages are awesome. They have a lot of brilliant design decisions, the most mainstream ones are Algebraic Data Type (ADT) and pattern matching.

Simple ReasonML example:

/* Algebraic Data Type */
type coolString = 
  | Uppercase(string)
  | Concat(string, string)
  | Empty;

/* pattern matching */
let string_of_cool_string = fun
  | Uppercase(str) => String.uppercase_ascii(str)
  | Concat(str1, str2) => str1 ++ str2
  | Empty => ""
Enter fullscreen mode Exit fullscreen mode

You can think of ADT as enum with additional data/payload and pattern matching as switch by the payloadfull enums.

Naive TypeScript implementation would be:

type UpperCase = {
  key: 'UPPERCASE',
  value: string,
}

type Concat = {
  key: 'CONCAT',
  value: [string, string],
}

type Empty = {
  key: 'EMPTY',
}

const makeUpperCase = (value: string): UpperCase => ({
  key: 'UPPERCASE',
  value,
});

const makeConcat = (value: [string, string]): Concat => ({
  key: 'CONCAT',
  value,
});

const makeEmpty = (value: string): Empty => ({
  key: 'EMPTY',
});

type CoolString = UpperCase | Concat | Empty;

const match = (action: CoolString): string => {
  switch (action.key) {
    case 'UPPERCASE':
      return action.value.toUpperCase();
    case 'CONCAT':
      return action.value.join('');
    case 'EMPTY':
      return '';
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the TypeScript version is very verbose. The goal of this article is to present one of the possible solutions for lowering the number of lines of code while keeping everything simple and having all static type features.

First, we will write simple type that will represent ADT option and function to create options

type Option<Key extends string, Value = undefined> = {
  key: Key,
  value: Value,
};

const makeOption = <Key extends string, Value = undefined>(
  key: Key,
) => (
  value: Value,
): Option<Key, Value> => ({
  key,
  value,
});
Enter fullscreen mode Exit fullscreen mode

Having code above usage will be:

const Upper = makeOption<'upper', string>('upper')
const Concat = makeOption<'concat', [string, string]>('concat');
const Empty = makeOption<'empty'>('empty');

type CoolString
  = ReturnType<typeof Upper>
  | ReturnType<typeof Concat>
  | ReturnType<typeof Empty>

const match = (coolString: CoolString): string => {
  switch (coolString.key) {
    case 'upper':
      return coolString.value.toUpperCase();
    case 'concat':
      return coolString.value.join('');
    case 'empty':
      return '';
  }
}
Enter fullscreen mode Exit fullscreen mode

This way we have extracted boilerplate, so ADT pattern became much cleaner.

Discussion (0)

pic
Editor guide