├── config.d.ts ├── index.d.ts ├── LICENSE ├── README.md └── typescriptle.d.ts /config.d.ts: -------------------------------------------------------------------------------- 1 | export type WordNumber = 3122; 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Guess } from "./typescriptle"; 2 | 3 | type GuessResult = Guess<["", "", "", "", "", ""]>; 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Johannes Lumpe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⬛ 🟨 🟩 TypeScriptle - Wordle in TypeScript's type system 2 | 3 | ## How to play 4 | 5 | Simply clone this repo and open the project in an editor which supports TypeScript's language server. I'd recommend VSCode with the [Docs View extension](https://marketplace.visualstudio.com/items?itemName=bierner.docs-view) extension. It allows for a nice Wordle-like grid in the sidebar when looking at the result type, like so: 6 | 7 | ![TypeScriptle in VSCode](https://user-images.githubusercontent.com/3470207/158505041-21baad1c-01a2-4745-9e3d-25cec40b1dcd.png) 8 | 9 | From here on pick a random number in `config.d.ts` (the max is 12947) and start guessing in `index.d.ts`. Hovering over `GuessResult` will show you the current status in TypeScript's type information. 10 | 11 | ## How? 12 | 13 | TypeScript has such a great type system, that it made it almost trivially easy to implement this. The main two things this project relies on are template strings and recursive conditional types. Check out the annotated source in `typescriptle.d.ts` - that's what you're here for anyways if you're reading this ;). 14 | 15 | ## Why? 16 | 17 | Purely because I thought it would be fun to implement a Wordle-like game in TypeScript's type system. 18 | 19 | ## FAQ 20 | 21 | ### Does the game ensure that you don't guess more than six times? 22 | 23 | Yes it does. `Guess` does not accept more than six strings for its input tuple. Nothing stops you from using the `Guess` type multiple times though ;) 24 | -------------------------------------------------------------------------------- /typescriptle.d.ts: -------------------------------------------------------------------------------- 1 | import { WordNumber } from "./config"; 2 | import { WordList } from "./wordlist"; 3 | 4 | // Our word list is a large array of strings. Here we are converting it into a union. 5 | // This allows us easily to check if a word is valid via the `extends` keyword. 6 | type Words = WordList[number]; 7 | 8 | // The split type is used to turn a word, both a guess and our solution, into an array of characters. 9 | // This is done via template literal types. These are types which allow us to match on the pattern of a 10 | // string and extract parts from it. Below we can see that we split our string `T` by `Delimiter`, which by 11 | // default is an empty string. Using an empty string will effectively split a string into all its individual characters. 12 | // The `infer` keyword is used to tell TypeScript to figure out what the type at the given position is. In our case 13 | // it means that TypeScript will extract our characters. 14 | type Split = 15 | // If a string has characters both before and after the delimiter 16 | // we extract the parts around the delimiter, add the part in front of it 17 | // to our list of strings and then recursively call the Split type again with the 18 | // the rest of our string. The result of this we spread into the array we're returning. 19 | // This works just like array spreading in JavaScript. 20 | T extends `${infer Head}${Delimiter}${infer Tail}` 21 | ? [Head, ...Split] 22 | : // This check deals with empty strings to ensure that they do not end up in our list of 23 | // characters. `B` matches on an empty string, so this will match `'A'`, but not `''` 24 | T extends `${infer A}${infer B}` 25 | ? [A] 26 | : []; 27 | 28 | // The join type is the opposite of our above split type. In this case we are not allowing for a delimiter 29 | // to be added back in. We are simply concatenating all elements in `T` back together. 30 | // `...infer Tail` is the equivalent of JavaScript's `rest` operator (`...`). 31 | // The below type checks if our input `T` is a list which has 1 or more items and if so, 32 | // extract the first item, place it into a template string and then recursively call `Join` again 33 | // with the rest of the list. Once we have extracted all our elements from the list, we have an empty 34 | // list and will return an empty string, terminating the recursion. 35 | // The helper functions `CastToString` and `CastToStringList` are used because TypeScript does not know that 36 | // `Head` and `Tail` are actually of type `string` and `string[]` respectively, even though `T` has to be a `string[]`. 37 | // Instead of casting it would also be fine to add two additional `extend` checks, one for `Head` and one for `Tail`. 38 | // This would increase the ternary nesting though and since we can be certain that these are strings, casting is 39 | // the easier option. 40 | type Join = T extends [infer Head, ...infer Tail] 41 | ? `${CastToString}${Join>}` 42 | : ""; 43 | 44 | // These are just utility types to make the conditional types below more readable 45 | type CharacterNotFound = ` ⬛ ${Character} `; 46 | type CharacterAtCorrectPosition = ` 🟩 ${Character} `; 47 | type CharacterFound = ` 🟨 ${Character} `; 48 | type EmptyWordPlaceholder = " ⬛ ⬛ ⬛ ⬛ ⬛ "; 49 | type InvalidWord = " INVALID WORD "; 50 | 51 | // This type will check for the existence of a character in a list of characters. 52 | // It does this by recursively checking each charater in the list `SplitWorkd` against our 53 | // character `CharacterToSearchFor`. Depending on the result we return the appropriate 54 | // utility type. 55 | type ContainsCharacter< 56 | SplitWord extends string[], 57 | CharacterToSearchFor extends string 58 | > = SplitWord extends [infer CurrentCharacter, ...infer RestOfWord] 59 | ? // Character exists in solution, just not at the right spot 60 | CharacterToSearchFor extends CurrentCharacter 61 | ? CharacterFound 62 | : RestOfWord extends string[] 63 | ? // Recursively check the rest of the characters until we run out or find a match 64 | ContainsCharacter 65 | : // Wrong character 66 | CharacterNotFound 67 | : // Wrong character 68 | CharacterNotFound; 69 | 70 | // This type is used to determine whether `Word` is a valid word from our word list. 71 | type ValidWord = Word extends Words ? true : false; 72 | 73 | // As mentioned above, these are useful to help TypeScript hint at a given type, when 74 | // we are certain that a given type must be either a string or a string list. 75 | // We will always hit the first case, where `T` matches our predicate, so we will always 76 | // return `T`. The fact that we else return the generic, wider type allows us to keep 77 | // TypeScript from yelling at us about the type not being compatible wherever we expect 78 | // either a `string` or `string[]`. 79 | type CastToStringList = T extends string[] ? T : string[]; 80 | type CastToString = T extends string ? T : string; 81 | 82 | // This type is our "entry" type for comparing two lists of characters. 83 | // `GuessCharacterList` are the characters of the word that we guessed 84 | // and `SolutionCharacterList` are the individual characters of the word we're looking for. 85 | // Just as in our types above, we rely heavily on conditional types. 86 | type CompareWords< 87 | GuessCharacterList extends string[], 88 | SolutionCharacterList extends string[], 89 | // The full list of characters for the solution which will remain untouched. 90 | // This is needed for checking existence of a character _somewhere_ in the whole solution 91 | FullSolutionCharacterList = SolutionCharacterList 92 | > = 93 | // We need to have at least a list with one character to guess 94 | GuessCharacterList extends [ 95 | infer CurrentGuessedLetter, 96 | ...infer RestOfGuessedWord 97 | ] 98 | ? // We also need at least one character in the list of characters of our solution 99 | SolutionCharacterList extends [ 100 | infer CurrentSolutionLetter, 101 | ...infer RestOfSolution 102 | ] 103 | ? [ 104 | // If both letters at the current position match, the guessed letter is correct 105 | CurrentGuessedLetter extends CurrentSolutionLetter 106 | ? CharacterAtCorrectPosition> 107 | : // If they didn't match, the guessed letter might be used somewhere in the solution 108 | ContainsCharacter< 109 | CastToStringList, 110 | CastToString 111 | >, 112 | // We call `CompareWords` recursively, with both the remaining guess characters and remaining 113 | // solution characters. It is importat that we also explicitly pass in `FullSolutionCharacterList` 114 | // so that it doesn't get reassigned to ` CastToStringList` the next recursion. 115 | // If that happend we would not be able to check the whole list for general existence of a character 116 | // as the list would shrink with each recursion. 117 | ...CompareWords< 118 | CastToStringList, 119 | CastToStringList, 120 | FullSolutionCharacterList 121 | > 122 | ] 123 | : [] 124 | : []; 125 | 126 | // This wrapper type solely exists to hide the solution word from the expanded type that 127 | // TypeScript reports. The usage of a generic and conditional type prevents expansion. 128 | type CompareWithSolution = Guess extends string[] 129 | ? CompareWords> 130 | : never; 131 | 132 | type GuessWord = 133 | // If we haven't guessed anything yet, we just show empty squares 134 | Guess extends "" 135 | ? EmptyWordPlaceholder 136 | : // Only if we guessed a generally valid word, we check how many characters matched 137 | ValidWord> extends true 138 | ? Join>>> 139 | : // Otherwise we report that the word was invalid 140 | InvalidWord; 141 | 142 | // Basic utility type to give us a more declarative version of "string or nothing" 143 | type Maybe = T | undefined; 144 | 145 | // This is our entry type which is a wrapper around `GuessWord` to enforce 146 | // a maximum of six guesses. Additionally it returns the results in an object 147 | // which gives us always a nice gridlike view due to each property 148 | // being shown on its own line. 149 | export type Guess< 150 | T extends [ 151 | Maybe, 152 | Maybe, 153 | Maybe, 154 | Maybe, 155 | Maybe, 156 | Maybe 157 | ] 158 | > = { 159 | // Since `T` must be assignable to our tuple of six strings 160 | // we can just access the values directly via their index 161 | "Guess 1": GuessWord; 162 | "Guess 2": GuessWord; 163 | "Guess 3": GuessWord; 164 | "Guess 4": GuessWord; 165 | "Guess 5": GuessWord; 166 | "Guess 6": GuessWord; 167 | }; 168 | --------------------------------------------------------------------------------