diff --git a/AGENTS.md b/AGENTS.md index c48f61e34..7856fd3d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -531,6 +531,29 @@ Avoid `as` type assertions (except `as const`). Treat them like `unsafe` in Rust justified. They silence the type checker and hide real bugs; if a cast is needed, it usually means the types or the code structure should be improved instead. +The JSDoc equivalent, an inline `/** @type {T} */ (expr)` cast, carries the same +hazard and the same rule: avoid it. Prefer annotating a separate `const` +declaration instead of casting an expression inline — + +```js +/** @type {ReadonlyMap} */ +const empty = new Map() +``` + +rather than + +```js +mapSet(/** @type {ReadonlyMap} */ (new Map()), 'a', 1) +``` + +— because the declaration form documents the variable's intended type and lets +the compiler check the initializer against it (closer to `satisfies`), while the +inline form silently overrides whatever the compiler inferred, exactly like `as`. +An exception applies when the original TypeScript source used `as`: a mechanical +`.f.ts` → `.f.mjs` migration may carry the assertion over as an inline +`@type`-cast rather than block on a redesign, but should still prefer the +declaration form when it is a straightforward rewrite. + #### Avoid type predicates Avoid TypeScript type predicates (`(x: T): x is U`). They are error-prone: the diff --git a/CHANGELOG.md b/CHANGELOG.md index 35aa43d7c..4b6407f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ history. ## Unreleased +- **BREAKING CHANGES:** `fjs/types/map` migrates from authored TypeScript + (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) under the stage-1 + TypeScript-to-mjs migration — importers must use the `.f.mjs` specifier + [#1476](https://github.com/functionalscript/functionalscript/pull/1476) - **BREAKING CHANGES:** `fjs/types/btree` migrates from authored TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) under the stage-1 TypeScript-to-mjs migration — importers must use the `.f.mjs` specifier diff --git a/fjs/types/map/module.f.mjs b/fjs/types/map/module.f.mjs new file mode 100644 index 000000000..b3c5eec9c --- /dev/null +++ b/fjs/types/map/module.f.mjs @@ -0,0 +1,42 @@ +/** + * Persistent map operations built on ordered collections. + * + * @module + */ + +/** + * @template T + * @param {Iterable} x + * @param {Iterable} y + * @returns {Iterable} + */ +const concat = (x, y) => ({ + *[Symbol.iterator]() { + yield* x + yield* y + } +}) + +/** + * @template T + * @param {Iterable} i + * @param {(x: T) => boolean} p + * @returns {Iterable} + */ +const filter = (i, p) => ({ + *[Symbol.iterator]() { + for (const x of i) { + if (p(x)) { yield x } + } + } +}) + +/** + * @type {(map: ReadonlyMap, k: K, v: V) => ReadonlyMap} + */ +export const mapSet = (map, k, v) => new Map(concat(map, [[k, v]])) + +/** + * @type {(map: ReadonlyMap, k: K) => ReadonlyMap} + */ +export const mapDelete = (map, k) => new Map(filter(map, ([xk]) => xk !== k)) diff --git a/fjs/types/map/module.f.ts b/fjs/types/map/module.f.ts deleted file mode 100644 index 043d52ac9..000000000 --- a/fjs/types/map/module.f.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Persistent map operations built on ordered collections. - * - * @module - */ -const concat = (x: Iterable, y: Iterable): Iterable => ({ - *[Symbol.iterator]() { - yield* x - yield* y - } -}) - -const filter = (i: Iterable, p: (x: T) => boolean): Iterable => ({ - *[Symbol.iterator]() { - for (const x of i) { - if (p(x)) { yield x } - } - } -}) - -export const mapSet = (map: ReadonlyMap, k: K, v: V): ReadonlyMap => - new Map(concat(map, [[k, v]])) - -export const mapDelete = (map: ReadonlyMap, k: K): ReadonlyMap => - new Map(filter(map, ([xk]) => xk !== k)) diff --git a/fjs/types/map/proof.f.ts b/fjs/types/map/proof.f.mjs similarity index 78% rename from fjs/types/map/proof.f.ts rename to fjs/types/map/proof.f.mjs index 6b171815b..09dc549d1 100644 --- a/fjs/types/map/proof.f.ts +++ b/fjs/types/map/proof.f.mjs @@ -1,4 +1,4 @@ -import { mapSet, mapDelete } from './module.f.ts' +import { mapSet, mapDelete } from './module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' export const proof = { @@ -13,7 +13,9 @@ export const proof = { assertEq(map.size, 0, 'error') }, deleteOneOfMany: () => { - const m0 = mapSet(new Map(), 'a', 1) + /** @type {ReadonlyMap} */ + const empty = new Map() + const m0 = mapSet(empty, 'a', 1) const m1 = mapSet(m0, 'b', 2) const result = mapDelete(m1, 'a') assertEq(result.get('b'), 2) diff --git a/fjs/types/todo/169.md b/fjs/types/todo/169.md index f24e16b27..6704abcf2 100644 --- a/fjs/types/todo/169.md +++ b/fjs/types/todo/169.md @@ -3,16 +3,16 @@ **Priority:** P3 **Status:** open -`fjs/types/map/module.f.ts` defines two private generator helpers solely to feed -`new Map(...)`: +`fjs/types/map/module.f.mjs` defines two private generator helpers solely to +feed `new Map(...)`: -```ts -// map/module.f.ts:6 -const concat = (x: Iterable, y: Iterable): Iterable => ({ +```js +// map/module.f.mjs:13 +const concat = (x, y) => ({ *[Symbol.iterator]() { yield* x; yield* y } }) -// map/module.f.ts:13 -const filter = (i: Iterable, p: (x: T) => boolean): Iterable => ({ +// map/module.f.mjs:26 +const filter = (i, p) => ({ *[Symbol.iterator]() { for (const x of i) { if (p(x)) { yield x } } } })