From c679fc5b944888234cab3c5c1f1bbf24e89c907e Mon Sep 17 00:00:00 2001 From: Vovan-VE Date: Thu, 21 Dec 2023 00:35:53 +0800 Subject: [PATCH] Fix: `updateDefault` inference unnecessary `V | D` --- CHANGELOG.md | 6 ++++ package.json | 2 +- src/_internal.ts | 5 +++ src/updateDefault.ts | 19 ++++++++++- test/deep/updateDefaultDeep.test.ts | 46 ++++++++++++++++++++++++++ test/updateDefault.test.ts | 51 +++++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/_internal.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b77c23..8582b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.0.1 (2023-12-21) + +- Fix: `updateDefault()` since `1.2.0` in some cases could inference unnecessary + annoying `V | D` in untyped callback param even when `D` is already a subtype + of `V`. See test cases for details. + ## 2.0.0 (2023-12-14) - **BREAKING**: Drop Node < 18. diff --git a/package.json b/package.json index f5bc394..e04186d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cubux/readonly-map", - "version": "2.0.0", + "version": "2.0.1", "description": "Functions to work with read-only maps", "keywords": [ "map", diff --git a/src/_internal.ts b/src/_internal.ts new file mode 100644 index 0000000..e34af2f --- /dev/null +++ b/src/_internal.ts @@ -0,0 +1,5 @@ +/** + * @see https://github.com/microsoft/TypeScript/issues/14829#issuecomment-504042546 + * TS>=5.4 + */ +export type NoInfer = [T][T extends any ? 0 : never]; diff --git a/src/updateDefault.ts b/src/updateDefault.ts index 45464af..6fb19b8 100644 --- a/src/updateDefault.ts +++ b/src/updateDefault.ts @@ -1,5 +1,22 @@ +import { NoInfer } from './_internal'; import set from './set'; +interface updateDefaultFn { + ( + map: ReadonlyMap, + key: K, + defaultValue: NoInfer, + updater: (prev: V, key: K, map: ReadonlyMap) => V, + ): ReadonlyMap; + + ( + map: ReadonlyMap, + key: K, + defaultValue: V | D, + updater: (prev: V | D, key: K, map: ReadonlyMap) => V, + ): ReadonlyMap; +} + /** * Creates new map from input `map` by updating value in the given `key` with * the given callback `updater()`. The given `defaultValue` will be used as @@ -45,4 +62,4 @@ function updateDefault( ); } -export default updateDefault; +export default updateDefault as updateDefaultFn; diff --git a/test/deep/updateDefaultDeep.test.ts b/test/deep/updateDefaultDeep.test.ts index 202679f..7c7e970 100644 --- a/test/deep/updateDefaultDeep.test.ts +++ b/test/deep/updateDefaultDeep.test.ts @@ -63,3 +63,49 @@ it("does nothing when it's nothing to change", () => { expect(noop).toHaveBeenNthCalledWith(2, origA); expect(prev).toEqual(orig); }); + +describe('default is subtype', () => { + interface Patch { + n?: number; + s?: string; + } + type M = ReadonlyMap>; + + const orig: M = new Map(); + + it('full', () => { + const next = updateDefaultDeep(orig, [10, 20], { n: 1, s: '' }, (p) => { + const { n, s } = p; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); + + it('partial', () => { + const next = updateDefaultDeep(orig, [10, 20], { n: 1 }, (p) => { + const { n, s } = p; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); + + it('{}', () => { + const next = updateDefaultDeep(orig, [10, 20], {}, (p) => { + const { n, s } = p; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); + + it('alt', () => { + const next = updateDefaultDeep(orig, [10, 20], null, (p) => { + const { n, s } = p || {}; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); +}); diff --git a/test/updateDefault.test.ts b/test/updateDefault.test.ts index 548c9f6..ff090e1 100644 --- a/test/updateDefault.test.ts +++ b/test/updateDefault.test.ts @@ -66,3 +66,54 @@ it("alternate 'default' type", () => { const expected2: ReadonlyMap = next2; expect(expected2.get(42)).toBe('x*'); }); + +describe('default is subtype', () => { + interface Patch { + n?: number; + s?: string; + } + type M = ReadonlyMap; + + const orig: M = new Map(); + + it('full', () => { + const next = updateDefault(orig, 10, { n: 1, s: '' }, (p) => { + const { n, s } = p; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); + + it('partial', () => { + const next = updateDefault(orig, 10, { n: 1 }, (p) => { + // `p` must be `Patch` here, not `Patch | { n:number }`, + // so `s` is known + const { n, s } = p; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); + + it('{}', () => { + const next = updateDefault(orig, 10, {}, (p) => { + // `p` must be `Patch` here, not `Patch | {}`, + // so `n` and `s` are known + const { n, s } = p; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); + + it('alt', () => { + const next = updateDefault(orig, 10, null, (p) => { + // `p` must be `Patch | null` here + const { n, s } = p || {}; + return { n, s }; + }); + const m: M = next; + expect(m.has(10)).toBeTruthy(); + }); +});