Skip to content

Commit 7a7bd39

Browse files
committed
fix(types): fix matrix types
These have been broken for at least a year. I guess related to a TS upgrade some time in that period. This change overhauls the way we create vectors to bring back type-safety.
1 parent f978b54 commit 7a7bd39

File tree

8 files changed

+85
-72
lines changed

8 files changed

+85
-72
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@commitlint/cli": "19.8.1",
6060
"@commitlint/config-conventional": "19.8.1",
6161
"@semantic-release/git": "10.0.1",
62+
"@total-typescript/tsconfig": "^1.0.4",
6263
"@types/node": "24.3.3",
6364
"benny": "3.7.1",
6465
"commitizen": "4.3.1",
@@ -81,7 +82,7 @@
8182
"body-max-line-length": [
8283
1,
8384
"always",
84-
100
85+
200
8586
]
8687
}
8788
},

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
File renamed without changes.
Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, expect, test } from "vitest";
1+
import { assertType, describe, expect, test } from "vitest";
22

33
import { makeMatrix } from "../index";
4+
import type { Matrix, Vector } from "../types";
45

56
const testCases = [[6], [1, 1], [3, 2, 3], [1, 4, 5, 2], [2, 4, 2, 4, 2]];
67

@@ -38,3 +39,38 @@ describe("makeMatrix", () => {
3839
});
3940
});
4041
});
42+
43+
describe("types", () => {
44+
test("Vector", () => {
45+
assertType<Vector<[1, 1, 1, 1]>>([0, 0, 0, 0]);
46+
47+
// @ts-expect-error
48+
assertType<Vector<[1, 1, 1, 1]>>([0, 0, 0, 0, 0]);
49+
// @ts-expect-error
50+
assertType<Vector<[1, 1, 1, 1]>>([0, 0, 0]);
51+
});
52+
53+
test("Matrix", () => {
54+
assertType<Matrix<[1], "test">>(["test"]);
55+
assertType<Matrix<[1, 1], "test">>([["test"], ["test"]]);
56+
assertType<Matrix<[1, 1], number>>([[2], [1]]);
57+
58+
// @ts-expect-error
59+
assertType<Matrix<[1, 1], "test">>(["test", "test"]);
60+
});
61+
62+
test("makeMatrix", () => {
63+
assertType<string[][][][]>(makeMatrix([1, 1, 1, 1], "hello"));
64+
assertType<number[][]>(makeMatrix([4, 5], ([a]) => a));
65+
assertType<string[]>(makeMatrix([4], v => v[0]?.toString()));
66+
assertType<unknown[]>(makeMatrix([] as number[]));
67+
68+
assertType<(0 | 1)[]>(
69+
makeMatrix([1], () => (Math.random() > 0.5 ? (0 as const) : (1 as const)))
70+
);
71+
72+
assertType<{ x: number; y: number; z: number }[][][]>(
73+
makeMatrix([4, 5, 6], ([x, y, z]) => ({ x, y, z }))
74+
);
75+
});
76+
});

src/__tests__/types.test.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,29 @@ import type { Matrix, Vector } from "./types.js";
1212
* const twoDRandomNumberArray = makeMatrix([1, 4], () => Math.random()); // A 1x4 array containing random numbers
1313
* const twoDVectorArray = makeMatrix([3, 3], vector => vector.join()); // A 3x3 array containing each point's co-ordinate as a string
1414
*/
15-
const makeMatrix = <D extends number, T>(
16-
dimensions: Vector<D>,
15+
const makeMatrix = <const D extends readonly number[], T>(
16+
dimensions: D,
1717
initialValues: T | ((currentPosition: Vector<D>) => T) | null = null
1818
): Matrix<D, T> => {
19-
const initialPosition = Array(dimensions.length).fill(0) as Vector<D>;
20-
return _makeMatrix(dimensions, initialValues, initialPosition);
19+
const initialPosition = Array(dimensions.length).fill(0);
20+
return _makeMatrix(dimensions, initialValues, initialPosition as Vector<D>);
2121
};
2222

2323
/**
2424
* Recursively creates a matrix (depth first), and keeps track of the current position's vector.
2525
*/
26-
function _makeMatrix<D extends number, T>(
27-
dimensions: Vector<D>,
26+
function _makeMatrix<const D extends readonly number[], T>(
27+
dimensions: D,
2828
initialValues: T | ((currentPosition: Vector<D>) => T) | null,
29-
currentPosition: Vector<D>
29+
currentPosition: number[]
3030
): Matrix<D, T> {
3131
const [currentDimensionLength, ...remainingDimensions] = dimensions;
3232
const currentDimension = currentPosition.length - remainingDimensions.length - 1;
3333

3434
return [...Array(currentDimensionLength)].map((_, i) => {
3535
currentPosition[currentDimension] = i;
3636
return remainingDimensions.length > 0
37-
? _makeMatrix(remainingDimensions as Vector<D>, initialValues, currentPosition)
37+
? _makeMatrix(remainingDimensions as unknown as D, initialValues, currentPosition)
3838
: initialValues instanceof Function
3939
? initialValues(currentPosition.slice() as Vector<D>)
4040
: initialValues;

src/types.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
/**
2-
* A typed multidimensional array returned by `makeMatrix`.
2+
* Stops intellisense from expanding types and exposing their more complicated internals to users.
3+
*
4+
* @see {@link https://github.com/microsoft/TypeScript/issues/31940} for discussion
35
*/
4-
export type Matrix<D extends number, T = unknown> = Brand<_Matrix<D, T, []>>;
6+
type Brand<D> = D & {};
57

68
/**
7-
* Recursively constructs a `Matrix` type and keeps track of the number of recursions.
9+
* A typed multidimensional array.
810
*
911
* @remarks
10-
* When a `_Matrix` is created without knowing it's exact size or number of dimensions, such
12+
* When a `Matrix` is created without knowing it's exact size or number of dimensions, such
1113
* as when creating one dynamically, it's impossible recursively construct a type for it.
1214
* Instead, we short circuit the type to resolve to `unknown[]`. This ensures an array of at
1315
* least one dimension is returned, but is unable to provide any more safety than that.
1416
*/
15-
type _Matrix<
16-
D extends number,
17-
T = unknown,
18-
RecursionCountArray extends number[] = [],
19-
> = number extends D
20-
? unknown[]
21-
: D extends RecursionCountArray["length"]
22-
? T
23-
: _Matrix<D, T[], [...RecursionCountArray, D]>;
17+
export type Matrix<D extends readonly number[], T = unknown> = Brand<
18+
number extends D[number] ? unknown[] : BuildMatrix<D, T>
19+
>;
20+
21+
/**
22+
* Recursively builds a matrix type based on an array of dimensions
23+
*/
24+
type BuildMatrix<D extends readonly number[], T> = D extends readonly []
25+
? T
26+
: D extends readonly [infer _First, ...infer Rest]
27+
? Rest extends readonly number[]
28+
? BuildMatrix<Rest, T>[]
29+
: never
30+
: unknown[];
2431

2532
/**
2633
* An array of numbers with defined length, corresponding to points in a `Matrix`.
@@ -33,13 +40,15 @@ type _Matrix<
3340
* creating one dynamically, we short circuit the type to resolve to `number[]` so that
3441
* `makeMatrix` can accept a dynamically created `dimensions` parameter.
3542
*/
36-
export type Vector<D extends number> = Brand<
37-
number extends D ? number[] : [number, ...number[]] & { readonly length: D }
43+
export type Vector<D extends readonly number[]> = Brand<
44+
number extends D[number] ? number[] : BuildTuple<D["length"], number>
3845
>;
3946

4047
/**
41-
* Stops intellisense from expanding types and exposing their more complicated internals to users.
42-
*
43-
* @see {@link https://github.com/microsoft/TypeScript/issues/31940} for discussion
48+
* Recursively builds a tuple of a specific length with a given type
4449
*/
45-
type Brand<D> = D & {};
50+
type BuildTuple<
51+
Length extends number,
52+
T,
53+
Acc extends T[] = [],
54+
> = Acc["length"] extends Length ? Acc : BuildTuple<Length, T, [...Acc, T]>;

tsconfig.json

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
{
2-
"$schema": "https://json.schemastore.org/tsconfig",
3-
// Taken from @total-typescript/tsconfig/bundler/no-dom/library, which doesn't work well with ts-node
4-
"compilerOptions": {
5-
/* Base Options: */
6-
"esModuleInterop": true,
7-
"skipLibCheck": true,
8-
"target": "es2022",
9-
"allowJs": true,
10-
"resolveJsonModule": true,
11-
"moduleDetection": "force",
12-
"isolatedModules": true,
13-
"verbatimModuleSyntax": true,
14-
/* Strictness */
15-
"strict": true,
16-
"noUncheckedIndexedAccess": true,
17-
"noImplicitOverride": true,
18-
/* If NOT transpiling with TypeScript: */
19-
"module": "preserve",
20-
"noEmit": true,
21-
/* If your code doesn't run in the DOM: */
22-
"lib": ["es2022"]
23-
},
2+
"extends": "@total-typescript/tsconfig/bundler/no-dom/library",
243
"include": ["src"],
25-
"exclude": ["node_modules", "**/__tests__/*"]
4+
"exclude": ["node_modules"]
265
}

0 commit comments

Comments
 (0)