{"id":14651534,"url":"https://github.com/gruhn/typescript-sudoku","last_synced_at":"2025-04-05T07:06:57.231Z","repository":{"id":253846899,"uuid":"668907185","full_name":"gruhn/typescript-sudoku","owner":"gruhn","description":"Playing Sudoku in TypeScript while the type checker highlights mistakes.","archived":false,"fork":false,"pushed_at":"2024-08-20T08:33:54.000Z","size":838,"stargazers_count":494,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-29T06:06:49.395Z","etag":null,"topics":["sudoku","type-theory","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gruhn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-20T22:06:58.000Z","updated_at":"2025-03-11T19:38:45.000Z","dependencies_parsed_at":"2024-12-08T01:04:36.104Z","dependency_job_id":"c9ba580b-bcab-4554-afdc-df7149d60d74","html_url":"https://github.com/gruhn/typescript-sudoku","commit_stats":null,"previous_names":["gruhn/typescript-sudoku"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruhn%2Ftypescript-sudoku","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruhn%2Ftypescript-sudoku/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruhn%2Ftypescript-sudoku/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruhn%2Ftypescript-sudoku/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gruhn","download_url":"https://codeload.github.com/gruhn/typescript-sudoku/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247299832,"owners_count":20916190,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["sudoku","type-theory","typescript"],"created_at":"2024-09-11T05:01:21.841Z","updated_at":"2025-04-05T07:06:57.196Z","avatar_url":"https://github.com/gruhn.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# TypeScript Sudoku\n\nThis is an experiment to precisely define a Sudoku type.\nThe goal is that we can play Sudoku in TypeScript while the type checker complains about mistakes.\nThis is not about implementing a Sudoku solver.\nJust about writing unnecessarily complicated type definitions.\nFor the final result, check out [sudoku_v2.ts](./sudoku_v2.ts).\n\n![demo video: final approach](./sudoku_v2_demo.gif)\n\nAs a first approximation we can define the type as an array of numbers:\n\n```typescript\ntype Sudoku = number[]\n```\n\nThis permits all valid Sudokus, but also allows many *invalid* Sudokus.\n\n```typescript\nconst invalidSudoku: Sudoku = [ -1, 7.5, 9, 9 ]\n```\nFirst, all array elements have to be integers in the range 1 to 9.\nSecond, Sudokus are 9-by-9 grids so we need an array with exactly 81 elements.\nThat's easy enough:\n\n```typescript\ntype Cell = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9\n\ntype Sudoku = [\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n  Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell,\n]\n```\nBut the interesting part is: how do we enforce the Sudoku rules?\nCurrently, this still type checks:\n\n```typescript\nconst invalidSudoku: Sudoku = [\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n  1, 1, 1, 1, 1, 1, 1, 1, 1,\n]\n```\n\nWe have to make sure that\n\n * all rows are duplicate free, \n * all columns are duplicate free and\n * each of the 3-by-3 squares is also duplicate free.\n\nFor simplicity, let's just consider a 3-cell Sudoku for now:\n\n```typescript\ntype BabySudoku = [ Cell, Cell, Cell ]\n```\nHow can we require that the three cells all have different values?\nThe definition of a cell is dependent on other cells, \nso we need a way to reference individual cells.\nBy introducing a type parameter for every cell,\nwe can refer to cells by name:\n\n```typescript\ntype BabySudoku\u003cX1 extends Cell, X2 extends Cell, X3 extends Cell\u003e = [ \n  X1, X2, X3 \n]\n```\n\nGuess how many parameters the full 9-by-9 Sudoku needs.\n\nA problem is, we have to instantiate these type parameters when we create `BabySudoku` values:\n\n```typescript\nconst s: BabySudoku\u003c1, 2, 3\u003e = [ 1, 2, 3 ]\n```\n\nThis is redundant.\nThe type parameters should get inferred from the value.\nType parameters on functions are automatically inferred:\n\n```typescript\nconst func = \u003cT\u003e(val: T) =\u003e val\n\nfunc\u003cstring\u003e(\"hello\") // type parameter can be provided ...\nfunc(\"hello\")         // ... but not necessary\n```\n\nSo as a workaround we can define the type in the argument of some dummy function:\n\n```typescript\nconst babySudoku = \u003cX1 extends Cell,  X2 extends Cell,  X3 extends Cell \u003e(\n  grid: [ X1, X2, X3 ]\n) =\u003e grid\n```\n\nWith that we can construct Sudokus, \nwithout instantiating type parameters:\n\n```typescript\nconst s = babySudoku([1, 2, 3])\n```\n\nThe runtime behavior of `babySudoku` is boring.\nIt just returns it's argument unchanged (aka. the identity function).\n\nThis is a bit awkward but I haven't found a better way so far.\n\n## Constraints using `Exclude`\n\nWe still need to ensure that all cells are different.\nOne way to do it, is with the built-in utility type [`Exclude`](https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers).\n\n```typescript\nconst babySudoku = \u003c\n  X1 extends Cell,\n  X2 extends Cell, \n  X3 extends Cell\n\u003e(\n  grid: [ \n    Exclude\u003cX1, X2 | X3\u003e, \n    Exclude\u003cX2, X1 | X3\u003e, \n    Exclude\u003cX3, X1 | X2\u003e, \n  ]\n) =\u003e grid\n```\n\nThis works! \nWhat's particularly nice about this approach is that the type checker highlights exactly the cells that are in conflict:\n\n![demo: first approach](./sudoku_v1_demo.png)\n\nA downside is that the [full type definition](sudoku_v1.ts) is very large.\nFor each cell we have to explicitly list the other cells that are required to be different.\nWriting that out manually is tedious.\nI ended up [creating a script](generate_sudoku_v1.ts) just to print out the type definition.\n\nI came up with another approach that is more complex but also more flexible. \nBefore we can talk about that we need to talk about the types `unknown` and `never`.\n\n## Interlude: `unknown` and `never`\n\n`never` is the empty type, making it a subtype of everything.\nThere is no value that has type `never`. \nSo you always get a type error no matter what you write on the right-hand side of `const value: never = ???`.\nThat is, unless the assignment is unreachable or you use some malicous type cast like: \n```typescript\nconst value: never = \"obviously not never\" as never\n```\n`never` is useful to strategically provoke a type error.\nThe plan is to formulate the Sudoku type in such a way, \nthat it is equal to `never` IF constraints are violated.\n\n`unknown` is a supertype of everything, similar to `any` ([What's the difference between `unknown` and `any`?](https://stackoverflow.com/a/51439876)). \n\n\u003cimg alt=\"type hierarchie\" src=\"./type-hierarchie.png\" height=\"300px\" /\u003e\n\nI find it useful to think of `unknown` and `never` as type-level analogs of `true` and `false`.\nIn combination with `unknown` and `never`, union types (`A | B`) and intersection types (`A \u0026 B`)\nbehave just like boolean OR (`a || b`) and boolean AND (`a \u0026\u0026 b`).\nNotice the syntactic similarity of these operators.\nFor example, `unknown | never` is the same as `unknown`, because building the union of *absolutely-everything* and *absolutely-nothing* gives back *absolutely-everything*.\nAnalogously, `true || false` is `true`.\n\nLet's define type aliases, to make the relationship more obvious:\n```typescript\ntype True = unknown\ntype False = never\n```\nWith that we have:\n\n| Term-level                    | Type-level                  | \n| ----------------------------- | --------------------------- |\n| `true \u0026\u0026 true` is `true`      | `True \u0026 True` is `True`     |\n| `true \u0026\u0026 false` is `false`    | `True \u0026 False` is `False`   |\n| `true \\|\\| false` is `true`   | `True \\| False` is `True`   |\n| `false \\|\\| false` is `false` | `False \\| False` is `False` |\n| ...                           | ...                         |\n\nNow that we can talk about booleans on the type-level, \nwe can formulate arbitrary boolean constraints inside type definitions.\n\n## Constraints using Type-Level Predicates\n\nThe plan is to come up with some type-level boolean expression that describes the Sudoku constraints.\nAgain, let's stick to a 3-cell grid for now, where all cells have to be different.\nWe want a to define a type\n\n```typescript\ntype CheckSudokuConstraints\u003cX1, X2, X3\u003e = ??? // \"returns\" either `unknown` or `never`\n```\n\nAt this point we can think of `CheckSudokuConstraints` as a function that returns `true` or `false` (aka. a predicate).\nAn analogous term-level function would look like this:\n\n```typescript\nfunction checkSudokuConstraints(x1: Cell, x2: Cell, x3: Cell): boolean {\n   return ???\n}\n```\n\nOnce we know how to define `CheckSudokuConstraints` we build the intersection with the actual number grid:\n\n```typescript\n[ X1, X2, X3 ] \u0026 CheckSudokuConstraints\u003cX1, X2, X3\u003e\n```\n\nIF some Sudoku constraint is violated, then `CheckSudokuConstraints\u003cX1, X2, X3\u003e` \"returns\" `never` and we get:\n\n```typescript\n[ X1, X2, X3 ] \u0026 never             // ==\u003e never\n```\n\nThe intersection with *absolutely-nothing* is always *absolutely-nothing* again, \nso the whole definition \"collapses\" down to `never`.\n\n\u003cimg alt=\"Venn diagram: never intersection\" src=\"intersect_never.png\" height=\"250px\" /\u003e\n\nIF all Sudoku constraint are satisfied, \nthen `CheckSudokuConstraints\u003cX1, X2, X3\u003e` \"returns\" `unknown` and we get:\n\n```typescript\n[ X1, X2, X3 ] \u0026 unknown          // ==\u003e [ X1, X2, X3 ]\n```\n\nThe intersection with `unknown` just leaves the left-hand side alone.\n\n\u003cimg alt=\"Venn diagram: unknown intersection\" src=\"intersect_unknown.png\" height=\"250px\" /\u003e\n\n### Defining `CheckSudokuConstraints`\n\nFirst we need a type-level predicate to compare two cells.\nWe can use [conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) to check if two types are equal:\n```typescript\ntype Equal\u003cA, B\u003e = A extends B ? (B extends A ? unknown : never) : never\n// Equal\u003c3, 3\u003e    ==\u003e    unknown\n// Equal\u003c3, 4\u003e    ==\u003e    never\n```\nIf `A` is a subtype of `B` and `B` is a subtype of `A`, \nthen `A` and `B` must be the same type.\nThus, we return true (i.e. `unknown`). \nOtherwise, we return false (i.e. `never`).\n\nBy negating the logic, \nwe get a type-level predicate that tells us when two cells are different:\n\n```typescript\ntype Different\u003cA, B\u003e = A extends B ? (B extends A  ? never : unknown) : unknown\n// Different\u003c3, 3\u003e    ==\u003e    never\n// Different\u003c3, 4\u003e    ==\u003e    unkown\n```\n\nTo express that a bunch of cells are all different, we can go through all pairs.\nAgain, read the intersection type operator (`\u0026`) just like boolean AND:\n\n```typescript\n  Different\u003cX1, X2\u003e \u0026 Different\u003cX1, X3\u003e \n\u0026 Different\u003cX2, X1\u003e \u0026 Different\u003cX2, X3\u003e\n\u0026 Different\u003cX3, X1\u003e \u0026 Different\u003cX3, X2\u003e\n```\n\nActually, `Different` is symmetric, i.e. \n`Different\u003cX1, X2\u003e` and `Different\u003cX2, X1\u003e` are the same,\nso we can skip half the pairs:\n\n```typescript\nDifferent\u003cX1, X2\u003e \u0026 Different\u003cX1, X3\u003e \u0026 Different\u003cX2, X3\u003e\n```\n\nAnd that's already the definition of `CheckSudokuConstraints` if we only have three cells:\n\n```typescript\ntype CheckSudokuConstraints\u003cX1, X2, X3\u003e = Different\u003cX1, X2\u003e \u0026 Different\u003cX1, X3\u003e \u0026 Different\u003cX2, X3\u003e\n```\n\nFor the full 81-cell Sudoku, the number of cells to compare is getting a bit out of hand.\nWe want to express that the cells in each row, each column and each square are pairwise different.\nRows, columns and squares always have exactly 9 cells.\nSo we can define one more utility type that just checks pairwise difference of 9 arbitrary given cells:\n\n```typescript\ntype AllDifferent\u003cX1, X2, X3, X4, X5, X6, X7, X8, X9\u003e = \n    Different\u003cX1, X2\u003e \u0026 Different\u003cX1, X3\u003e \u0026 Different\u003cX1, X4\u003e\n  \u0026 Different\u003cX1, X5\u003e \u0026 Different\u003cX1, X6\u003e \u0026 Different\u003cX1, X7\u003e\n  \u0026 ...\n```\n\nFinally, we can define `CheckSudokuConstraints` for the full Sudoku:\n\n```typescript\ntype CheckSudokuConstraints\u003c\n  X11, X12, X13,  X14, X15, X16,  X17, X18, X19,\n  X21, X22, X23,  X24, X25, X26,  X27, X28, X29,\n  X31, X32, X33,  X34, X35, X36,  X37, X38, X39,\n\n  X41, X42, X43,  X44, X45, X46,  X47, X48, X49,\n  X51, X52, X53,  X54, X55, X56,  X57, X58, X59,\n  X61, X62, X63,  X64, X65, X66,  X67, X68, X69,\n\n  X71, X72, X73,  X74, X75, X76,  X77, X78, X79,\n  X81, X82, X83,  X84, X85, X86,  X87, X88, X89,\n  X91, X92, X93,  X94, X95, X96,  X97, X98, X99,\n\u003e = \n  // all 9 rows\n  \u0026 AllDifferent\u003cX11, X12, X13, X14, X15, X16, X17, X18, X19\u003e\n  \u0026 AllDifferent\u003cX21, X22, X23, X24, X25, X26, X27, X28, X29\u003e\n  \u0026 AllDifferent\u003cX31, X32, X33, X34, X35, X36, X37, X38, X39\u003e\n  \u0026 AllDifferent\u003cX41, X42, X43, X44, X45, X46, X47, X48, X49\u003e\n  \u0026 AllDifferent\u003cX51, X52, X53, X54, X55, X56, X57, X58, X59\u003e\n  \u0026 AllDifferent\u003cX61, X62, X63, X64, X65, X66, X67, X68, X69\u003e\n  \u0026 AllDifferent\u003cX71, X72, X73, X74, X75, X76, X77, X78, X79\u003e\n  \u0026 AllDifferent\u003cX81, X82, X83, X84, X85, X86, X87, X88, X89\u003e\n  \u0026 AllDifferent\u003cX91, X92, X93, X94, X95, X96, X97, X98, X99\u003e\n\n  // all 9 columns\n  \u0026 AllDifferent\u003cX11, X21, X31, X41, X51, X61, X71, X81, X91\u003e\n  \u0026 AllDifferent\u003cX12, X22, X32, X42, X52, X62, X72, X82, X92\u003e\n  \u0026 AllDifferent\u003cX13, X23, X33, X43, X53, X63, X73, X83, X93\u003e\n  \u0026 AllDifferent\u003cX14, X24, X34, X44, X54, X64, X74, X84, X94\u003e\n  \u0026 AllDifferent\u003cX15, X25, X35, X45, X55, X65, X75, X85, X95\u003e\n  \u0026 AllDifferent\u003cX16, X26, X36, X46, X56, X66, X76, X86, X96\u003e\n  \u0026 AllDifferent\u003cX17, X27, X37, X47, X57, X67, X77, X87, X97\u003e\n  \u0026 AllDifferent\u003cX18, X28, X38, X48, X58, X68, X78, X88, X98\u003e\n  \u0026 AllDifferent\u003cX19, X29, X39, X49, X59, X69, X79, X89, X99\u003e\n\n  // three upper squares\n  \u0026 AllDifferent\u003cX11, X12, X13, X21, X22, X23, X31, X32, X33\u003e\n  \u0026 AllDifferent\u003cX14, X15, X16, X24, X25, X26, X34, X35, X36\u003e\n  \u0026 AllDifferent\u003cX17, X18, X19, X27, X28, X29, X37, X38, X39\u003e\n\n  // three center squares\n  \u0026 AllDifferent\u003cX41, X42, X43, X51, X52, X53, X61, X62, X63\u003e\n  \u0026 AllDifferent\u003cX44, X45, X46, X54, X55, X56, X64, X65, X66\u003e\n  \u0026 AllDifferent\u003cX47, X48, X49, X57, X58, X59, X67, X68, X69\u003e\n\n  // three lower squares\n  \u0026 AllDifferent\u003cX71, X72, X73, X81, X82, X83, X91, X92, X93\u003e\n  \u0026 AllDifferent\u003cX74, X75, X76, X84, X85, X86, X94, X95, X96\u003e\n  \u0026 AllDifferent\u003cX77, X78, X79, X87, X88, X89, X97, X98, X99\u003e\n```\n\n## Incomplete Sudokus\n\nUntil now we have only described complete Sudokus, \nwhere every cell is already filled with an integer.\nTo actually play Sudoku, we need to allow empty cells.\n\nFor that we pick some dummy value to represent an empty cell.\nThis can be anything, as long as it's not a number from 1 to 9:\n\n```typescript\nconst _ = \"empty cell\"\n\ntype EmptyCell = typeof _\n```\n\nNow we *could* redefine `Cell` to include this value:\n\n```typescript\ntype Cell = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | EmptyCell // won't work\n```\n\nBut then the constraints also apply to empty cells \nand the type checker starts complaining about things like two empty cells in the same row.\nEmpty cells should be unconstrained and allowed everywhere.\nWe can do that by explicitly annotating each cell with `| EmptyCell`:\n\n```typescript\n[ X1 | EmptyCell, X2 | EmptyCell, X3 | EmptyCell ] \u0026 CheckSudokuConstraints\u003cX1, X2, X3\u003e\n```\nSo in each cell we either allow an empty cell or we allow an integer from 1-9 that is additionally constrained.\n\nFor the full type definition, check out [sudoku_v2.ts](./sudoku_v2.ts).\n\n## Conclusion\n\nThis is pretty useless.\nOne could try to implement a statically verified Sudoku solver based on these types:\n\n```typescript\nfunction solveSudoku(grid: IncompleteSudoku): CompleteSudoku { /* ... */ }\n```\n\nThis would give very high confidence in the implementations correctness.\nHowever, it's probably hard to convince the type checker that the code really matches the spec.\nEven then, error messages are not very friendly and, \ndepending on the TypeScript version, \nit can take multiple seconds to type check the code.\n\nNevertheless, I think it's interesting to see how much expressivity one can squeeze out of the type system.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgruhn%2Ftypescript-sudoku","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgruhn%2Ftypescript-sudoku","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgruhn%2Ftypescript-sudoku/lists"}