Building a Sudoku Validator
A functional approach to validating Sudoku puzzles with composable code
Written by Trevor Healy

When approaching a validation problem, I start by identifying the common pattern across all validation rules. In this case, rows, columns, and regions all share the same constraint: they must contain exactly 9 unique integers from 1 to 9.
This insight drives the solution architecture: build a single, testable validation utility, then compose three simple extraction functions that transform the board into lists that can be validated uniformly.
Foundation: isValidList
Start with the smallest, most testable unit. This function validates a single list—the atomic operation that all other validations reduce to.
const isValidList = (xs: number[]) =>
xs.length === 9 &&
new Set(xs).size === 9 &&
xs.every(n => Number.isInteger(n) && n >= 1 && n <= 9);Three checks: correct length, uniqueness via Set, and valid range. This function is trivial to test in isolation and becomes the building block for all validation logic.
Composable Extraction Utilities
With the validation logic isolated, the problem reduces to transforming the board into three sets of lists. Each extraction function is independently testable and returns an array of lists ready for uniform validation.

generateRows
The simplest case: rows are already in the correct structure.
const generateRows = (board: number[][]) => board;generateColumns
Transpose the board by mapping each column index across all rows.
const generateColumns = (board: number[][]) =>
board[0].map((_, x) => board.map(row => row[x]));generateRegions
Regions require the most thought. While you could use modulo arithmetic to calculate boundaries dynamically, that approach sacrifices readability for cleverness.
Since Sudoku has fixed bounds, we define region boundaries once in an array where each entry specifies the top-left and bottom-right coordinates.
const REGION_BOUNDS: [[number, number], [number, number]][] = [
[[0,0],[2,2]],
[[3,0],[5,2]],
[[6,0],[8,2]],
[[0,3],[2,5]],
[[3,3],[5,5]],
[[6,3],[8,5]],
[[0,6],[2,8]],
[[3,6],[5,8]],
[[6,6],[8,8]]
];With your dictionary defined, you can do a simple nested for loop to pull out the region numbers.
const generateRegions = (board: number[][]): number[][] => {
return REGION_BOUNDS.map(([[x0,y0],[x1,y1]]) => {
const region: number[] = [];
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
region.push(board[y][x]);
}
}
return region;
});
};This approach prioritizes clarity and maintainability. The boundaries are explicit, verifiable at a glance, and the extraction logic is straightforward. Any engineer can understand and modify this without deciphering mathematical relationships.
Composition
The final validation function composes the extraction utilities with the validation logic. Each extraction function returns an array of lists, and we validate all of them uniformly:
const isValidBoard = (board: number[][]) =>
generateRows(board).every(isValidList) &&
generateColumns(board).every(isValidList) &&
generateRegions(board).every(isValidList);This composition demonstrates the power of the decomposition: by reducing the problem to its smallest parts, the solution becomes a simple combination of well-tested, composable pieces.
Complete Code
Here's the complete validator in one place:
const isValidList = (xs: number[]) =>
xs.length === 9 &&
new Set(xs).size === 9 &&
xs.every(n => Number.isInteger(n) && n >= 1 && n <= 9);
const generateRows = (board: number[][]) => board;
const generateColumns = (board: number[][]) =>
board[0].map((_, x) => board.map(row => row[x]));
const REGION_BOUNDS: [[number, number], [number, number]][] = [
[[0,0],[2,2]],
[[3,0],[5,2]],
[[6,0],[8,2]],
[[0,3],[2,5]],
[[3,3],[5,5]],
[[6,3],[8,5]],
[[0,6],[2,8]],
[[3,6],[5,8]],
[[6,6],[8,8]]
];
const generateRegions = (board: number[][]): number[][] => {
return REGION_BOUNDS.map(([[x0,y0],[x1,y1]]) => {
const region: number[] = [];
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
region.push(board[y][x]);
}
}
return region;
});
};
const isValidBoard = (board: number[][]) =>
generateRows(board).every(isValidList) &&
generateColumns(board).every(isValidList) &&
generateRegions(board).every(isValidList);