String Deduplication on the Type Level

Michael Poteat
Written by
Michael Poteat
Published on: 
September 20, 2021
4
 min read
Table of Contents

Foreword

This is a cross-published article on type-level string manipulation. At Weekend, we use Typescript to power our voice experiences. We highly value engineering excellence and safety, and do our best to create robust systems that the compiler can validate as correct.

This code does not represent what we do for our games, but does serve as a fun example of the strong capabilities of Typescript’s type system. The original article was published on code.lol.


String Deduplication on the Type Level

The string deduplication problem is a canonical one within computer science, serving a similar purpose as fizz-buzz in terms of being an example of a simple problem that a reasonably knowledgeable practitioner should be able to solve with minimal effort.

The problem appears in a few variants, but briefly one such variant is to remove duplicate letters in a given string, such that the string then has only one instance of any given letter.

I thought this problem would be a particularly interesting case study into Typescript 4.1’s powerful literal string types.


String Splitting

The first step of any string algorithm is to split the string into units that can be processed individually. In this case, we split a given string literal into an array of strings, each of length 1.


type Split = S extends ""
? []
: S extends `${infer C}${infer R}`
? [C, ...Split]
: never;

type Result = Split<"Foobar">; // :: ["F", "o", "o", "b", "a", "r"]


Split is defined essentially as a nested conditional type (using the ternary syntax), that recurses into itself to define the tuple type. The C type is inferred to be the first character of S, and the R type is inferred to be the rest. (corresponding to Character and Rest respectively).


String Joining

Another important capability when working with strings is the ability to collapse an array of strings into one string, via concatenation. On the type level, we can implement this in a similar fashion as the above Split type.


type Join = T extends []
? ""
: T extends [infer Head, ...infer Tail]
? Head extends string
? `${Head}${Join}`
: never
: never;


type Result = Split<"Foobar">; // :: ["F", "o", "o", "b", "a", "r"]

With this pattern, we are essentially performing a reduce operation in a similar manner as you might do when implementing it in a combinator form, e.g. using the Y combinator instead of a reduce function as such.

For example, this is how the equivalent meaning would be written on the value level:


const join = (x: string[]): string =>
x.length === 0 ? "" : `${x[0]}${join(x.slice(1))}`;

Tuple Uniqueness

The next feature we need is the ability to enforce tuples to be unique, only allowing the first instance of a given element to exist, and filtering away all others. We do this by keeping track of letters we have already seen (R), and conditionally outputting to an output string tuple type (O).


type Invert> = {
[key in keyof T as T[key] extends string ? T[key] : never]: key;
};

type Unique<
T extends string[],
R extends Record = Record,
O extends string[] = []
> = T extends []
? O
: T extends [infer Head, ...infer Tail]
? Unique<
Tail extends string[] ? Tail : [],
R & Invert<{ _: Head }>,
Head extends string ? (R[Head] extends string ? O : [...O, Head]) : []
>
: never;

This is somewhat complicated by a limitation of TS 4.1, namely the ability to construct an object with a given key, whereby the key type is a literal type. The way we work around that now is via Invert, which switches the keys and values of an object type.

Putting it Together

The string deduplication problem is essentially a composition between various simple operations. With the fundamental utilities we’ve defined, the actual deduplication type is concise:


type DedupeString = Join>>;

type Result = DedupeString<"banana">; // "ban"


Circle logo with colorful vertical sound bars and the words Song Quiz in white on dark background.
Play Song Quiz on TV
Think you know your music?
Guess songs from short clips. On your TV.
Try for free
Jeopardy! logo with white text over a globe showing continents in orange and purple shades.
Play Jeopardy! on TV
Step behind the podium
Real Jeopardy! New clues added every day.
Try for free
Wheel of Forture
Play wheel Of Fortune on TV
Spin the wheel from your couch
Solve daily word puzzles with your voice.
Try for free
20 Questions
Play 20 Questions on TV
Twenty questions. Zero excuses.
20 Questions against a smart riddlemaster.
Try for free
Karaoke
Play Karaoke on TV
Know the words? Prove it.
Sing along to your favorite songs. On your TV.
Try for free
Play CoComelon on TV
Big smiles, zero effort.
Sing along to your favorite songs. On your TV.
Try for free
Wit's End Icon
Play Whit's End on TV
Your choices. Your story.
Fantasy RPG where you control the story.
Try for free
Play on Weekend
Game night starts on your TV.
Beloved games like Jeopardy! and Wheel of Fortune
  • No controller needed
  • Free for 7 days
  • Works on Roku, Fire TV, Samsung & LG
Handwritten style word 'Weekend' in black script on a transparent background.
Start a game night on your TV
Play Jeopardy!, Wheel of Fortune, Song Quiz and more on your TV. No controller needed.
Play Now!
Available on Roku, Fire TV, Samsung, and LG.
Free for 7 days. Cancel anytime.
Play games on your TV