diff --git a/package.json b/package.json index 724596d..64f0461 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "eslint --fix", "test": "npm run test:unit && npm run test:typescript", "test:unit": "c8 tape test/*.js", - "test:typescript": "tsd" + "test:typescript": "tstyche" }, "repository": { "type": "git", @@ -56,7 +56,7 @@ "neostandard": "^0.13.0", "tape": "^5.7.5", "c8": "^11.0.0", - "tsd": "^0.33.0" + "tstyche": "^7.0.0" }, "files": [ "LICENSE", @@ -71,4 +71,4 @@ "object", "immutable" ] -} +} \ No newline at end of file diff --git a/types/index.test-d.ts b/types/index.test-d.ts deleted file mode 100644 index 58235ac..0000000 --- a/types/index.test-d.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { expectAssignable, expectError, expectType } from 'tsd' -import { deepmerge, type DeepMergeFn, type DeepMergeAllFn, DeepMergeDefinedFn, DeepMergeAllDefinedFn } from '.' - -// onlyDefinedProperties: true tests -// When source has optional properties (Partial), merging into a full type should preserve required-ness - -expectAssignable(deepmerge()) -expectType(deepmerge()) - -expectType(deepmerge()({ a: 'a' }, { b: 'b' }).a) -expectType(deepmerge()({ a: 'a' }, { b: 'b' }).b) -expectType(deepmerge()({ a: 2 }, { b: 'b' }).a) -expectType(deepmerge()({ a: 2 }, { b: 'b' }).b) -expectType(deepmerge()({ a: 2 }, { a: 'b' }).a) - -expectError(deepmerge(1)) -expectError(deepmerge({ symbols: 2 })) -expectError(deepmerge({ symbol: 2 })) - -expectAssignable(deepmerge({ symbols: true })) -expectType(deepmerge({ symbols: true })) - -expectType(deepmerge()('string', { a: 'string' }).a) -expectType(deepmerge()(1, { a: 'string' }).a) - -expectType(deepmerge()({ a: 'string' }, 'string')) -expectType(deepmerge()({ a: 'string' }, 1)) -expectType(deepmerge()({ a: 'string' }, new Date())) -expectType(deepmerge()({ a: 'string' }, /a/g)) -expectType<{}>(deepmerge()(/a/, {})) - -expectType(deepmerge()({ a: 'string' }, { a: 1 }).a) -expectType(deepmerge()({ a: 'string' }, { b: 1 }).a) -expectType(deepmerge()({ a: 'string' }, { }).a) -expectType(deepmerge()({ a: 'string' }, { b: 1 }).b) -expectType<{ a: string }>(deepmerge()({ a: { a: 'string' } }, { b: 1 }).a) -expectType<{ a: number }>(deepmerge()({ a: { a: 'string' } }, { a: { a: 1 } }).a) -expectType<{ a: number, b: string }>(deepmerge()({ a: { a: 'string' } }, { a: { a: 1, b: 'string' } }).a) -expectType<{ a: number, b: string }>(deepmerge()({ a: { a: { a: 'string' } } }, { a: { a: 1, b: 'string' } }).a) -expectType<{ a: { a: string, b: string } }>(deepmerge()({ a: { a: { a: 'string' } } }, { a: { a: { b: 'string' } } }).a) -expectType(deepmerge()({ a: [1, 2, 3, 4] }, { a: 'string' }).a) -expectType(deepmerge()({ a: [1, 2, 3, 4] }, { a: [1, 2, 3, 4] }).a) -expectType<(number | string)[]>(deepmerge()({ a: [1, 2, 3, 4] }, { a: ['a'] }).a) -expectType<(number | string)[]>(deepmerge()({ a: [1, 2, 3, 4] as readonly number[] }, { a: ['a'] }).a) -expectType<[1, 2, 3, 4, 'a']>(deepmerge()({ a: [1, 2, 3, 4] as const }, { a: ['a'] as const }).a) -expectType>(deepmerge()({ a: [1] }, { a: [2] }).a) -expectType<{ b: number[] }>(deepmerge()({ a: { b: {} } }, { a: { b: [2] } }).a) -expectType<{ b: Date }>(deepmerge()({ a: { b: {} } }, { a: { b: new Date() } }).a) -expectType<{ b: RegExp }>(deepmerge()({ a: { b: {} } }, { a: { b: /abc/g } }).a) -expectType(deepmerge()({ a: { b: {} } }, new Date())) -expectType>(deepmerge()({ a: { b: {} } }, new Map())) - -expectAssignable(deepmerge({ all: true })) -expectType(deepmerge({ all: true })) -expectAssignable(deepmerge({ all: true, symbols: true })) -expectType(deepmerge({ all: true, symbols: true })) -expectType(deepmerge({ all: true, symbols: true })({ a: 'a' }).a) -expectType(deepmerge({ all: true, symbols: true })({ a: 'a' }, { b: 'a' }).a) -expectType(deepmerge({ all: true, symbols: true })({ a: 'a' }, { b: 'a' }).b) -expectType(deepmerge({ all: true, symbols: true })({ a: 'a' }, { a: 2 }).a) -expectType(deepmerge({ all: true, symbols: true })({ a: 'a' }, 2)) -expectType(deepmerge({ all: true, symbols: true })({ a: 'a' }, 'string')) - -expectError(deepmerge({ mergeArray: function () { } })) -expectError(deepmerge({ mergeArray: function () { return () => 'test' } })) -deepmerge({ - mergeArray: function (options) { - expectType<(value: any) => any>(options.clone) - const clone = options.clone - return function (target, source) { - return clone(target.concat(source)) - } - } -}) -deepmerge({ - mergeArray: function () { - return function (_target, source) { - return source - } - } -}) -deepmerge({ - isMergeableObject: function (value) { - return true - } -}) - -expectType<(value: any) => boolean>(deepmerge.isMergeableObject) -expectError(deepmerge.isMergeableObject = function () { return false }) - -expectType(deepmerge({ onlyDefinedProperties: true })) -expectType(deepmerge({ all: true, onlyDefinedProperties: true })) - -// Basic case: merging partial into full type preserves required properties -interface FullOptions { - apiUrl: string - timeout: number - nested: { - enabled: boolean - value: string - } -} - -type PartialOptions = Partial - -// With onlyDefinedProperties: true, merging Partial into T should return T -// because undefined values from source are ignored at runtime -const fullTarget: FullOptions = { apiUrl: 'url', timeout: 100, nested: { enabled: true, value: 'test' } } -const partialSource: PartialOptions = { timeout: 200 } - -const mergedDefined = deepmerge({ onlyDefinedProperties: true })(fullTarget, partialSource) -expectType(mergedDefined.apiUrl) -expectType(mergedDefined.timeout) -expectType<{ enabled: boolean; value: string }>(mergedDefined.nested) - -// Result should be assignable to FullOptions (the target type) -expectAssignable(mergedDefined) - -// Contrast with regular merge (without onlyDefinedProperties) - source undefined could override -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const mergedRegular = deepmerge()(fullTarget, partialSource) - -// Deep partial merging -type DeepPartialOptions = { - apiUrl?: string - timeout?: number - nested?: { - enabled?: boolean - value?: string - } -} - -const deepPartialSource: DeepPartialOptions = { nested: { enabled: false } } -const mergedDeep = deepmerge({ onlyDefinedProperties: true })(fullTarget, deepPartialSource) -expectType(mergedDeep.apiUrl) -expectType(mergedDeep.timeout) -// Nested properties should also preserve required-ness from target -expectAssignable<{ enabled: boolean; value: string }>(mergedDeep.nested) - -// all mode with onlyDefinedProperties -const mergedAll = deepmerge({ all: true, onlyDefinedProperties: true })( - fullTarget, - { timeout: 300 } as PartialOptions, - { apiUrl: 'newUrl' } as PartialOptions -) -expectAssignable(mergedAll) diff --git a/types/index.tst.ts b/types/index.tst.ts new file mode 100644 index 0000000..b1d56ae --- /dev/null +++ b/types/index.tst.ts @@ -0,0 +1,139 @@ +import { expect } from 'tstyche' +import { deepmerge, type DeepMergeFn, type DeepMergeAllFn, DeepMergeDefinedFn, DeepMergeAllDefinedFn } from '.' + +expect(deepmerge()).type.toBeAssignableTo() +expect(deepmerge()).type.toBe() + +expect(deepmerge()({ a: 'a' }, { b: 'b' }).a).type.toBe() +expect(deepmerge()({ a: 'a' }, { b: 'b' }).b).type.toBe() +expect(deepmerge()({ a: 2 }, { b: 'b' }).a).type.toBe() +expect(deepmerge()({ a: 2 }, { b: 'b' }).b).type.toBe() +expect(deepmerge()({ a: 2 }, { a: 'b' }).a).type.toBe() + +expect(deepmerge).type.not.toBeCallableWith(1) +expect(deepmerge).type.not.toBeCallableWith({ symbols: 2 }) +expect(deepmerge).type.not.toBeCallableWith({ symbol: 2 }) + +expect(deepmerge({ symbols: true })).type.toBeAssignableTo() +expect(deepmerge({ symbols: true })).type.toBe() + +expect(deepmerge()('string', { a: 'string' }).a).type.toBe() +expect(deepmerge()(1, { a: 'string' }).a).type.toBe() + +expect(deepmerge()({ a: 'string' }, 'string')).type.toBe() +expect(deepmerge()({ a: 'string' }, 1)).type.toBe() +expect(deepmerge()({ a: 'string' }, new Date())).type.toBe() +expect(deepmerge()({ a: 'string' }, /a/g)).type.toBe() +expect(deepmerge()(/a/, {})).type.toBe<{}>() + +expect(deepmerge()({ a: 'string' }, { a: 1 }).a).type.toBe() +expect(deepmerge()({ a: 'string' }, { b: 1 }).a).type.toBe() +expect(deepmerge()({ a: 'string' }, { }).a).type.toBe() +expect(deepmerge()({ a: 'string' }, { b: 1 }).b).type.toBe() +expect(deepmerge()({ a: { a: 'string' } }, { b: 1 }).a).type.toBe<{ a: string }>() +expect(deepmerge()({ a: { a: 'string' } }, { a: { a: 1 } }).a).type.toBe<{ a: number }>() +expect(deepmerge()({ a: { a: 'string' } }, { a: { a: 1, b: 'string' } }).a).type.toBe<{ a: number, b: string }>() +expect(deepmerge()({ a: { a: { a: 'string' } } }, { a: { a: 1, b: 'string' } }).a).type.toBe<{ a: number, b: string }>() +expect(deepmerge()({ a: { a: { a: 'string' } } }, { a: { a: { b: 'string' } } }).a).type.toBe<{ a: { a: string, b: string } }>() +expect(deepmerge()({ a: [1, 2, 3, 4] }, { a: 'string' }).a).type.toBe() +expect(deepmerge()({ a: [1, 2, 3, 4] }, { a: [1, 2, 3, 4] }).a).type.toBe() +expect(deepmerge()({ a: [1, 2, 3, 4] }, { a: ['a'] }).a).type.toBe<(number | string)[]>() +expect(deepmerge()({ a: [1, 2, 3, 4] as readonly number[] }, { a: ['a'] }).a).type.toBe<(number | string)[]>() +expect(deepmerge()({ a: [1, 2, 3, 4] as const }, { a: ['a'] as const }).a).type.toBe<[1, 2, 3, 4, 'a']>() +expect(deepmerge()({ a: [1] }, { a: [2] }).a).type.toBe>() +expect(deepmerge()({ a: { b: {} } }, { a: { b: [2] } }).a).type.toBe<{ b: number[] }>() +expect(deepmerge()({ a: { b: {} } }, { a: { b: new Date() } }).a).type.toBe<{ b: Date }>() +expect(deepmerge()({ a: { b: {} } }, { a: { b: /abc/g } }).a).type.toBe<{ b: RegExp }>() +expect(deepmerge()({ a: { b: {} } }, new Date())).type.toBe() +expect(deepmerge()({ a: { b: {} } }, new Map())).type.toBe>() + +expect(deepmerge({ all: true })).type.toBeAssignableTo() +expect(deepmerge({ all: true })).type.toBe() +expect(deepmerge({ all: true, symbols: true })).type.toBeAssignableTo() +expect(deepmerge({ all: true, symbols: true })).type.toBe() +expect(deepmerge({ all: true, symbols: true })({ a: 'a' }).a).type.toBe() +expect(deepmerge({ all: true, symbols: true })({ a: 'a' }, { b: 'a' }).a).type.toBe() +expect(deepmerge({ all: true, symbols: true })({ a: 'a' }, { b: 'a' }).b).type.toBe() +expect(deepmerge({ all: true, symbols: true })({ a: 'a' }, { a: 2 }).a).type.toBe() +expect(deepmerge({ all: true, symbols: true })({ a: 'a' }, 2)).type.toBe() +expect(deepmerge({ all: true, symbols: true })({ a: 'a' }, 'string')).type.toBe() + +expect(deepmerge).type.not.toBeCallableWith({ mergeArray: function () {} }) +expect(deepmerge).type.not.toBeCallableWith({ + mergeArray: function () { + return () => 'test' + } +}) + +deepmerge({ + mergeArray: function (options) { + expect(options.clone).type.toBe<(value: any) => any>() + const clone = options.clone + return function (target, source) { + return clone(target.concat(source)) + } + } +}) +deepmerge({ + mergeArray: function () { + return function (_target, source) { + return source + } + } +}) +deepmerge({ + isMergeableObject: function (value) { + return true + } +}) + +expect(deepmerge.isMergeableObject).type.toBe<(value: any) => boolean>() + +expect(deepmerge({ onlyDefinedProperties: true })).type.toBe() +expect(deepmerge({ all: true, onlyDefinedProperties: true })).type.toBe() + +interface FullOptions { + apiUrl: string + timeout: number + nested: { + enabled: boolean + value: string + } +} + +type PartialOptions = Partial + +const fullTarget: FullOptions = { apiUrl: 'url', timeout: 100, nested: { enabled: true, value: 'test' } } +const partialSource: PartialOptions = { timeout: 200 } + +const mergedDefined = deepmerge({ onlyDefinedProperties: true })(fullTarget, partialSource) +expect(mergedDefined.apiUrl).type.toBe() +expect(mergedDefined.timeout).type.toBe() +expect(mergedDefined.nested).type.toBe<{ enabled: boolean; value: string }>() + +expect(mergedDefined).type.toBeAssignableTo() + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mergedRegular = deepmerge()(fullTarget, partialSource) + +type DeepPartialOptions = { + apiUrl?: string + timeout?: number + nested?: { + enabled?: boolean + value?: string + } +} + +const deepPartialSource: DeepPartialOptions = { nested: { enabled: false } } +const mergedDeep = deepmerge({ onlyDefinedProperties: true })(fullTarget, deepPartialSource) +expect(mergedDeep.apiUrl).type.toBe() +expect(mergedDeep.timeout).type.toBe() +expect(mergedDeep.nested).type.toBeAssignableTo<{ enabled: boolean; value: string }>() + +const mergedAll = deepmerge({ all: true, onlyDefinedProperties: true })( + fullTarget, + { timeout: 300 } as PartialOptions, + { apiUrl: 'newUrl' } as PartialOptions +) +expect(mergedAll).type.toBeAssignableTo()