Skip to content

Type-Level Wordle in TypeScript

Published On
Read Time
4 min

After type-level Hangman, here’s the next game: Wordle, implemented in TypeScript at the type level. Your guessed words are represented as a type, and the result is also a type. There’s no runtime code - just pure type manipulation.

Wordle game implemented in TypeScript at the type level

The colors work like in the real game: 🟩 means the letter is in the right spot, 🟨 means it’s in the word but in the wrong spot, and ⬜️ means it’s not in the word at all.

How many languages let you do this? TypeScript is impressive!

Implementation

Here’s the full code:

/* ------------------------------------------------------------ *\
   WORDLE IN TYPESCRIPT TYPES
\* ------------------------------------------------------------ */
type Input = [
  // Guess up to five times
];

// ⬇️ Hover over the type to see the result
type Result = Run<Input>;

// ---------------------------------------------------------------
//
//
//
//                         DO NOT CHEAT!
//
//
//
// ---------------------------------------------------------------

type Run<T> = T extends ValidInput ? Guess<TakeUntil<T, GameSolution>> : "Invalid Input";

type Guess<T extends ValidInput> = ObjectFromArray<{
  [K in keyof T]: GuessWord<Uppercase<T[K]>>;
}>;

type GuessWord<T extends string> = T extends ValidWord
  ? `${T}: ${ReplaceWithEmojis<
      Breath<ActualWordOf<ReplacePartialAndNonMatches<ReplaceMatches<[T, GameSolution]>>>>
    >}${T extends GameSolution ? "  🥳" : ""}`
  : `${T}: Unknown Word`;

type ReplaceMatches<T extends WordTuple> = T extends [
  `${infer ActualLetter}${infer ActualRest}`,
  `${infer ExpectedLetter}${infer ExpectedRest}`,
]
  ? ActualLetter extends ExpectedLetter
    ? Prefix<ReplaceMatches<[ActualRest, ExpectedRest]>, Match>
    : Prefix<ReplaceMatches<[ActualRest, ExpectedRest]>, ActualLetter, ExpectedLetter>
  : ["", ""];

type ReplacePartialAndNonMatches<T extends WordTuple> = T extends [
  `${infer ActualLetter}${infer ActualRest}`,
  infer Expected extends string,
]
  ? ActualLetter extends Match
    ? Prefix<ReplacePartialAndNonMatches<[ActualRest, Expected]>, ActualLetter, "">
    : Contains<ActualLetter, Expected> extends true
      ? Prefix<ReplacePartialAndNonMatches<[ActualRest, ReplaceFirst<ActualLetter, Expected>]>, PartialMatch, "">
      : Prefix<ReplacePartialAndNonMatches<[ActualRest, Expected]>, NoMatch, "">
  : T;

/* ------------------------------------------------------------ *\
   Input
\* ------------------------------------------------------------ */
type ValidInput = OptionalTuple<[string, string, string, string, string]>;

/* ------------------------------------------------------------ *\
   Word Tuple
\* ------------------------------------------------------------ */
type WordTuple = [actual: string, expected: string];

type ActualWordOf<T extends WordTuple> = T[0];

/**
 * Prefixes the tuple elements with the given prefix.
 */
type Prefix<T extends WordTuple, With extends string, WithExpected extends string = With> = [
  `${With}${T[0]}`,
  `${WithExpected}${T[1]}`,
];

/* ------------------------------------------------------------ *\
   Match types
\* ------------------------------------------------------------ */
type ValidWord = WordList[number];

type ValidateWord<T> = T extends ValidWord ? T : { error: "Invalid solution" };

// We use single code point characters while matching and map them later.
// Emojis would break `extends` checks.
type Match = "+";
type PartialMatch = "~";
type NoMatch = "-";

type EmojiMap = { "+": "🟩"; "~": "🟨"; "-": "⬜️" };

/**
 * Replaces the matching characters in T with emojis.
 * Every unknown character is kept as is.
 */
type ReplaceWithEmojis<T extends string> = T extends `${infer Char}${infer Rest}`
  ? `${Char extends keyof EmojiMap ? EmojiMap[Char] : Char}${ReplaceWithEmojis<Rest>}`
  : T;

/* ------------------------------------------------------------ *\
   Utils
\* ------------------------------------------------------------ */
/**
 * Adds white-spaces between each character of `T`.
 */
type Breath<T extends string> = T extends `${infer A}${infer B}` ? (B extends "" ? A : `${A} ${Breath<B>}`) : T;

/**
 * Converts the array `T` into an object.
 * Unlike arrays, objects are rendered multi-line in the hover preview in VS Code.
 */
type ObjectFromArray<T> = {
  [K in keyof T as K extends `${number}` ? K : never]: T[K];
};

/**
 * Checks if `Find` is within `Word`.
 */
type Contains<Find extends string, Word extends string> = Word extends `${string}${Find}${string}` ? true : false;

/**
 * Replaces the first occurrence of the character `Find` in `Word` with `With`.
 */
type ReplaceFirst<Find, Word, With extends string = "_"> = Word extends `${infer Letter}${infer Rest}`
  ? Find extends Letter
    ? `${With}${Rest}`
    : `${Letter}${ReplaceFirst<Find, Rest, With>}`
  : Word;

/**
 * Converts the tuple `T` into a union type from [] to `T`.
 * @example `OptionalTuple<[x, x, x]> === [] | [x] | [x, x] | [x, x, x]`
 */
type OptionalTuple<T extends unknown[]> = T extends [unknown, ...infer Rest] ? T | OptionalTuple<Rest> : T;

type TakeUntil<T extends ValidInput, Match> = T extends [infer Word, ...infer Rest extends ValidInput]
  ? Word extends Match
    ? [Word]
    : [Word, ...TakeUntil<Rest, Match>]
  : T;

/* ------------------------------------------------------------ *\
   Content
\* ------------------------------------------------------------ */
type WordList = [
  "CLASS",
  "CONST",
  "WHILE",
  "YIELD",
  "ASYNC",
  "AWAIT",
  "THROW",
  "CATCH",
  "FINAL",
  "SUPER",
  "RAISE",
  "MATCH",
  "FALSE",
  "BEGIN",
  "UNTIL",
  "DEFER",
  "MACRO",
  "TRAIT",
  "MIXIN",
  "PANIC",
  "EMPTY",
  "ISSET",
  "UNSET",
  "ARROW",
  "APPLY",
  "TYPES",
  "FLOAT",
  "SHORT",
  "ARRAY",
  "SLICE",
  "TUPLE",
  "ENUMS",
  "UNION",
  "BYTES",
  "CHARS",
  "STACK",
  "QUEUE",
  "HEAPS",
  "GRAPH",
  "NODES",
  "EDGES",
  "INDEX",
  "VALUE",
  "MODEL",
  "TABLE",
  "FIELD",
  "PARSE",
  "PRINT",
  "INPUT",
  "FETCH",
  "MERGE",
  "SPLIT",
  "EVERY",
  "FLOOR",
  "ROUND",
  "POWER",
  "SHIFT",
  "SPAWN",
  "FLUSH",
  "MOUNT",
  "BATCH",
  "TRACE",
  "DEBUG",
  "CLONE",
  "STASH",
  "PATCH",
  "RESET",
  "BLAME",
  "BLOBS",
  "FORKS",
  "BUILD",
  "STAGE",
  "REACT",
  "REDUX",
  "REMIX",
  "ASTRO",
  "MAVEN",
  "NGINX",
  "REDIS",
  "FLASK",
  "BABEL",
  "CMAKE",
  "CARGO",
  "NUGET",
  "KAFKA",
  "SPARK",
  "CONDA",
  "LINUX",
  "REGEX",
  "SWIFT",
  "SCALA",
  "OCAML",
  "COBOL",
  "BASIC",
  "MYSQL",
  "MONGO",
  "QUERY",
  "JOINS",
  "VIEWS",
  "CACHE",
  "TOKEN",
  "DELTA",
  "QUOTA",
  "CHUNK",
  "FRAME",
  "LAYER",
  "PROXY",
  "ROUTE",
  "SCOPE",
  "BLOCK",
  "STORE",
  "STATE",
  "PROPS",
  "HOOKS",
  "FORMS",
  "COLOR",
  "HOVER",
  "WIDTH",
  "ALIGN",
  "LABEL",
  "IMAGE",
  "HTTPS",
  "LINKS",
  "CHMOD",
  "MKDIR",
  "TOUCH",
  "WHICH",
  "SLEEP",
  "CLEAR",
  "ALIAS",
  "SHELL",
  "HOSTS",
  "PORTS",
  "PATHS",
  "GLOBS",
  "CRONS",
  "MUTEX",
  "LOGIC",
  "ERROR",
  "FATAL",
  "TESTS",
  "MOCKS",
  "SPECS",
  "LINTS",
  "OCTAL",
  "LOOPS",
  "FLAGS",
  "SOCKS",
];

type GameSolution = ValidateWord<"TYPES">;