From 169b336a4bcaef95e404dc098e0725ae71a6a229 Mon Sep 17 00:00:00 2001 From: Andrei Picus Date: Wed, 22 May 2024 11:04:46 +0200 Subject: [PATCH] fix: Fix `isPartial` not working with interfaces --- src/matchers/is-partial.ts | 16 ++++---- tests/types.spec.ts | 79 +++++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/matchers/is-partial.ts b/src/matchers/is-partial.ts index 82e470d..19f572e 100644 --- a/src/matchers/is-partial.ts +++ b/src/matchers/is-partial.ts @@ -15,7 +15,7 @@ type DeepPartial = T extends ObjectType const looksLikeObject = (value: unknown): value is ObjectType => isPlainObject(value); -const getExpectedObjectDiff = (actual: unknown, expected: ObjectType): object => +const getExpectedObjectDiff = (actual: unknown, expected: unknown): object => Object.fromEntries( getKeys(expected).map((key) => { const expectedValue = getKey(expected, key); @@ -33,10 +33,7 @@ const getExpectedObjectDiff = (actual: unknown, expected: ObjectType): object => }) ); -const getActualObjectDiff = ( - actual: unknown, - expected: ObjectType -): unknown => { +const getActualObjectDiff = (actual: unknown, expected: unknown): unknown => { const actualKeys = getKeys(actual); const expectedKeys = new Set(getKeys(expected)); const commonKeys = actualKeys.filter((key) => expectedKeys.has(key)); @@ -77,7 +74,7 @@ const getKey = (value: unknown, key: Property): unknown => // @ts-expect-error because we're fine with a runtime undefined value value?.[key]; -const isMatch = (actual: unknown, expected: ObjectType): boolean => { +const isMatch = (actual: unknown, expected: unknown): boolean => { const actualKeys = getKeys(actual); const expectedKeys = getKeys(expected); @@ -101,7 +98,7 @@ const isMatch = (actual: unknown, expected: ObjectType): boolean => { }); }; -const deepPrintObject = (value: ObjectType) => +const deepPrintObject = (value: unknown) => cloneDeepWith(value, (value) => { if (isMatcher(value)) { return value.toString(); @@ -129,7 +126,10 @@ const deepPrintObject = (value: ObjectType) => * @example * It.isPartial({ foo: It.isString() }) */ -export const isPartial = >( +// T is not constrained to ObjectType because of +// https://github.com/microsoft/TypeScript/issues/57810, +// but K is to avoid inferring non-object partials +export const isPartial = & ObjectType>( partial: K ): TypeMatcher => matches((actual) => isMatch(actual, partial), { diff --git a/tests/types.spec.ts b/tests/types.spec.ts index a2808cb..b67632f 100644 --- a/tests/types.spec.ts +++ b/tests/types.spec.ts @@ -50,15 +50,24 @@ it('type safety', () => { when(() => fnany()).thenResolve(23); } - function matcherSafety() { + function partialSafety() { const number = (x: number) => x; - number(It.isAny()); - // @ts-expect-error wrong matcher type - number(It.isString()); - // @ts-expect-error wrong matcher type - number(It.isPlainObject()); - // @ts-expect-error wrong matcher type - number(It.isArray()); + number( + It.isPartial( + // @ts-expect-error non-object can't be partial-ed + { toString: () => 'bar' } + ) + ); + number( + It.isPartial( + // @ts-expect-error non-object + 42 + ) + ); + + const numberArray = (x: number[]) => x; + // @ts-expect-error array is not an object + numberArray(It.isPartial({ length: 2 })); const nestedObject = (x: { foo: { bar: number; 42: string } }) => x; nestedObject(It.isPlainObject()); @@ -68,24 +77,28 @@ it('type safety', () => { // @ts-expect-error wrong nested property type It.isPartial({ foo: { bar: 'boo' } }) ); - // @ts-expect-error because TS can't infer the proper type - // See https://github.com/microsoft/TypeScript/issues/55164. - nestedObject(It.isPartial({ foo: It.isPartial({ bar: 1 }) })); - const numberArray = (x: number[]) => x; - numberArray(It.isArray()); - numberArray(It.isArray([1, 2, 3])); - numberArray(It.isArray([It.isNumber()])); - // @ts-expect-error wrong type of array - numberArray(It.isArray(['a'])); - // @ts-expect-error wrong nested matcher type - numberArray(It.isArray([It.isString()])); + nestedObject( + It.isPartial({ + // @ts-expect-error because TS can't infer the proper type + // See https://github.com/microsoft/TypeScript/issues/55164. + foo: It.isPartial({ bar: 1 }), + }) + ); + + interface InterfaceType { + foo: string; + } + const withInterface = (x: InterfaceType) => x; + withInterface(It.isPartial({ foo: 'bar' })); const object = (x: { foo: number }) => x; object(It.isPartial({ foo: It.isNumber() })); object( - // @ts-expect-error wrong nested matcher type - It.isPartial({ foo: It.isString() }) + It.isPartial({ + // @ts-expect-error wrong nested matcher type + foo: It.isString(), + }) ); const objectWithArrays = (x: { foo: { bar: number[] } }) => x; @@ -117,7 +130,7 @@ it('type safety', () => { map: Map; set: Set; arr: Array; - }) => {}; + }) => data; objectLikeValues({ // @ts-expect-error Maps are not objects map: It.isPlainObject(), @@ -134,9 +147,28 @@ it('type safety', () => { // @ts-expect-error Arrays are not objects arr: It.isPartial({}), }); + } - const string = (x: string) => string; + function matcherSafety() { + const number = (x: number) => x; + number(It.isAny()); + // @ts-expect-error wrong matcher type + number(It.isString()); + // @ts-expect-error wrong matcher type + number(It.isPlainObject()); + // @ts-expect-error wrong matcher type + number(It.isArray()); + + const numberArray = (x: number[]) => x; + numberArray(It.isArray()); + numberArray(It.isArray([1, 2, 3])); + numberArray(It.isArray([It.isNumber()])); + // @ts-expect-error wrong type of array + numberArray(It.isArray(['a'])); + // @ts-expect-error wrong nested matcher type + numberArray(It.isArray([It.isString()])); + const string = (x: string) => x; const startsWith = (expected: string) => It.matches((actual) => actual.startsWith(expected)); string(startsWith('foo')); @@ -157,6 +189,7 @@ it('type safety', () => { // @ts-expect-error because the value can be undefined. number(captureMatcher.value); // @ts-expect-error number is not string + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion string(captureMatcher.value!); } });