From ee8ecdd24c9b8640ce2be08b66ca6aee2f2f033c Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 9 Oct 2024 12:24:20 -0400 Subject: [PATCH 1/8] friendship ended with toMatchTypeOf toMatchObjectType + toExtend are my best friends now --- src/index.ts | 50 +++++++++++++++++++++++- src/utils.ts | 4 ++ test/__snapshots__/errors.test.ts.snap | 53 +++++++++++++------------- test/types.test.ts | 11 ++++++ test/usage.test.ts | 41 ++++++++++++-------- 5 files changed, 116 insertions(+), 43 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0070d52..2a421a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ import type { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import type {AValue, Extends, MismatchArgs, StrictEqualUsingTSInternalIdenticalToOperator} from './utils' +import type {AValue, Extends, IsUnion, MismatchArgs, Not, StrictEqualUsingTSInternalIdenticalToOperator} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './messages' // backcompat, consider removing in next major version @@ -36,6 +36,28 @@ export * from './utils' // backcompat, consider removing in next major version * {@linkcode expectTypeOf()} utility. */ export interface PositiveExpectTypeOf extends BaseExpectTypeOf { + toMatchObjectType: < + Expected extends IsUnion extends true + ? 'toMatchObject does not support union types' + : Not>> extends true + ? 'toMatchObject only supports object types' + : StrictEqualUsingTSInternalIdenticalToOperator< + Pick, + Expected + > extends true + ? unknown + : MismatchInfo, Expected>, + >( + ...MISMATCH: MismatchArgs< + StrictEqualUsingTSInternalIdenticalToOperator, Expected>, + true + > + ) => true + + toExtend extends true ? unknown : MismatchInfo>( + ...MISMATCH: MismatchArgs, true> + ): true + toEqualTypeOf: { /** * Uses TypeScript's internal technique to check for type "identicalness". @@ -118,8 +140,20 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf extends true ? unknown : MismatchInfo>( + ...MISMATCH: MismatchArgs, true> + ) => true + + /** + * @deprecated - use either `toMatchObject` or `toExtend` instead + * - use `toMatchObject` to performa a strict check on a subset of your type's keys + * - use `toExtend` to check if your type extends the expected type + */ toMatchTypeOf: { /** + * @deprecated - use either `toMatchObject` or `toExtend` instead + * - use `toMatchObject` to performa a strict check on a subset of your type's keys + * - use `toExtend` to check if your type extends the expected type * A less strict version of {@linkcode toEqualTypeOf | .toEqualTypeOf()} * that allows for extra properties. * This is roughly equivalent to an `extends` constraint @@ -147,6 +181,9 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf extends BaseExpectTypeOf extends BaseExpectTypeOf { + toMatchObjectType: ( + ...MISMATCH: MismatchArgs< + StrictEqualUsingTSInternalIdenticalToOperator, Expected>, + false + > + ) => true + + toExtend(...MISMATCH: MismatchArgs, false>): true + toEqualTypeOf: { /** * Uses TypeScript's internal technique to check for type "identicalness". @@ -933,6 +979,8 @@ export const expectTypeOf: _ExpectTypeOf = ( toMatchTypeOf: fn, toEqualTypeOf: fn, toBeConstructibleWith: fn, + toMatchObjectType: fn, + toExtend: fn, toBeCallableWith: expectTypeOf, extract: expectTypeOf, exclude: expectTypeOf, diff --git a/src/utils.ts b/src/utils.ts index bd90880..19a8326 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -227,3 +227,7 @@ export type TuplifyUnion> = * Convert a union like `1 | 2 | 3` to a tuple like `[1, 2, 3]`. */ export type UnionToTuple = TuplifyUnion + +export type IsTuple = Or<[Extends, Extends]> + +export type IsUnion = Not['length'], 1>> diff --git a/test/__snapshots__/errors.test.ts.snap b/test/__snapshots__/errors.test.ts.snap index 8b0540a..05a3730 100644 --- a/test/__snapshots__/errors.test.ts.snap +++ b/test/__snapshots__/errors.test.ts.snap @@ -6,6 +6,11 @@ exports[`usage.test.ts 1`] = ` 999 expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() ~~~~~~~~~~~ +test/usage.test.ts:999:999 - error TS2344: Type '{ b: number; }' does not satisfy the constraint '{ a: "Expected: never, Actual: number"; b: "Expected: number, Actual: never"; }'. + Property 'a' is missing in type '{ b: number; }' but required in type '{ a: "Expected: never, Actual: number"; b: "Expected: number, Actual: never"; }'. + +999 expectTypeOf<{a: number}>().toExtend<{b: number}>() + ~~~~~~~~~~~ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does not satisfy the constraint '{ a: number; b: "Expected: number, Actual: never"; }'. Types of property 'b' are incompatible. Type 'number' is not assignable to type '"Expected: number, Actual: never"'. @@ -16,29 +21,40 @@ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does Types of property 'b' are incompatible. Type 'number' is not assignable to type '"Expected: number, Actual: never"'. -999 expectTypeOf({a: 1}).toMatchTypeOf<{a: number; b: number}>() - ~~~~~~~~~~~~~~~~~~~~~~ +999 expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() + ~~~~~~~~~~~~~~~~~~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. -999 expectTypeOf().toMatchTypeOf() - ~~~~~ +999 expectTypeOf().toExtend() + ~~~~~ +test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. + Types of property 'edible' are incompatible. + Type 'boolean' is not assignable to type '"Expected: boolean, Actual: never"'. + +999 expectTypeOf().toMatchObjectType() + ~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. 999 expectTypeOf().toEqualTypeOf() ~~~~~ -test/usage.test.ts:999:999 - error TS2554: Expected 0 arguments, but got 1. +test/usage.test.ts:999:999 - error TS2344: Type '{ b: 1; }' does not satisfy the constraint '{ a: "Expected: never, Actual: number"; b: "Expected: literal number: 1, Actual: never"; }'. + Property 'a' is missing in type '{ b: 1; }' but required in type '{ a: "Expected: never, Actual: number"; b: "Expected: literal number: 1, Actual: never"; }'. -999 expectTypeOf({a: 1}).toMatchTypeOf({b: 1}) - ~~~~~~ +999 expectTypeOf({a: 1}).toExtend<{b: 1}>() + ~~~~~~ +test/usage.test.ts:999:999 - error TS2344: Type '{ b: 1; }' does not satisfy the constraint '"Expected: ..., Actual: boolean"'. + +999 expectTypeOf({a: 1}).toMatchObjectType<{b: 1}>() + ~~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. -999 expectTypeOf().toMatchTypeOf() - ~~~~~ +999 expectTypeOf().toExtend() + ~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. @@ -107,8 +123,8 @@ test/usage.test.ts:999:999 - error TS2349: This expression is not callable. ~~~~~~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'number' does not satisfy the constraint '"Expected: number, Actual: string"'. -999 expectTypeOf().toMatchTypeOf() - ~~~~~~ +999 expectTypeOf().toExtend() + ~~~~~~ test/usage.test.ts:999:999 - error TS2345: Argument of type '"xxl"' is not assignable to parameter of type '"xs" | "sm" | "md"'. 999 expectTypeOf>().exclude().toHaveProperty('xxl') @@ -284,20 +300,5 @@ test/usage.test.ts:999:999 - error TS2769: No overload matches this call. Argument of type '[this]' is not assignable to parameter of type 'MismatchArgs extends true ? unknown : MismatchInfo>, true>'. 999 expectTypeOf(this).toEqualTypeOf(this) - ~~~~ - -test/usage.test.ts:999:999 - error TS2769: No overload matches this call. - Overload 1 of 2, '(value: (Extends extends true ? unknown : MismatchInfo) & AValue, ...MISMATCH: MismatchArgs extends true ? unknown : MismatchInfo<...>>, true>): true', gave the following error. - Argument of type 'this' is not assignable to parameter of type '(Extends extends true ? unknown : MismatchInfo) & AValue'. - Type 'B' is not assignable to type '(Extends extends true ? unknown : MismatchInfo) & AValue'. - Type 'B' is not assignable to type '(Extends extends true ? unknown : MismatchInfo) & { [avalue]?: undefined; }'. - Type 'this' is not assignable to type '(Extends extends true ? unknown : MismatchInfo) & { [avalue]?: undefined; }'. - Type 'B' is not assignable to type '(Extends extends true ? unknown : MismatchInfo) & { [avalue]?: undefined; }'. - Type 'B' is not assignable to type 'Extends extends true ? unknown : MismatchInfo'. - Type 'this' is not assignable to type 'Extends extends true ? unknown : MismatchInfo'. - Type 'B' is not assignable to type 'Extends extends true ? unknown : MismatchInfo'. - Argument of type '[this]' is not assignable to parameter of type 'MismatchArgs extends true ? unknown : MismatchInfo>, true>'. - -999 expectTypeOf(this).toMatchTypeOf(this) ~~~~" `; diff --git a/test/types.test.ts b/test/types.test.ts index 654e352..ee9c8f9 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -847,3 +847,14 @@ test('Overload edge cases', () => { expectTypeOf().parameters.toEqualTypeOf<[] | [1]>() expectTypeOf().returns.toEqualTypeOf<1>() }) + +test('toMatchObjectType', () => { + expectTypeOf<{a: number}>().toMatchObjectType<{a: number}>() + expectTypeOf<{a: number}>().not.toMatchObjectType<{a: string}>() + expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: number}>() + + // @ts-expect-error + expectTypeOf().toMatchObjectType() + // @ts-expect-error + expectTypeOf<{a: number}>().toMatchObjectType<{a: string} | {a: number}>() +}) diff --git a/test/usage.test.ts b/test/usage.test.ts index 346857b..8708972 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -21,41 +21,52 @@ test('`.toEqualTypeOf` fails on excess properties', () => { expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() }) -test('To allow for extra properties, use `.toMatchTypeOf`. This is roughly equivalent to an `extends` constraint in a function type argument.', () => { - expectTypeOf({a: 1, b: 1}).toMatchTypeOf<{a: number}>() +test('To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in both types.', () => { + expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() }) -test('`.toEqualTypeOf` and `.toMatchTypeOf` both fail on missing properties', () => { +test('To check that a type extends another type, use `.toExtend`', () => { + expectTypeOf().toExtend() + expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() + // @ts-expect-error + expectTypeOf<{a: number}>().toExtend<{b: number}>() +}) + +test('`.toEqualTypeOf` and `.toExtend` both fail on missing properties', () => { // @ts-expect-error expectTypeOf({a: 1}).toEqualTypeOf<{a: number; b: number}>() // @ts-expect-error - expectTypeOf({a: 1}).toMatchTypeOf<{a: number; b: number}>() + expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() }) -test('Another example of the difference between `.toMatchTypeOf` and `.toEqualTypeOf`, using generics. `.toMatchTypeOf` can be used for "is-a" relationships', () => { +test('Another example of the difference between `.toExtend`, `.toMatchObjectType`, and `.toEqualTypeOf`. `.toExtend` can be used for "is-a" relationships', () => { type Fruit = {type: 'Fruit'; edible: boolean} type Apple = {type: 'Fruit'; name: 'Apple'; edible: true} - expectTypeOf().toMatchTypeOf() + expectTypeOf().toExtend() // @ts-expect-error - expectTypeOf().toMatchTypeOf() + expectTypeOf().toExtend() + + // @ts-expect-error - edible:boolean !== edible:true + expectTypeOf().toMatchObjectType() // @ts-expect-error expectTypeOf().toEqualTypeOf() }) test('Assertions can be inverted with `.not`', () => { - expectTypeOf({a: 1}).not.toMatchTypeOf({b: 1}) + expectTypeOf({a: 1}).not.toExtend<{b: 1}>() + expectTypeOf({a: 1}).not.toMatchObjectType<{b: 1}>() }) test('`.not` can be easier than relying on `// @ts-expect-error`', () => { type Fruit = {type: 'Fruit'; edible: boolean} type Apple = {type: 'Fruit'; name: 'Apple'; edible: true} - expectTypeOf().toMatchTypeOf() + expectTypeOf().toExtend() - expectTypeOf().not.toMatchTypeOf() + expectTypeOf().not.toExtend() expectTypeOf().not.toEqualTypeOf() }) @@ -136,8 +147,8 @@ test('More `.not` examples', () => { }) test('Detect assignability of unioned types', () => { - expectTypeOf().toMatchTypeOf() - expectTypeOf().not.toMatchTypeOf() + expectTypeOf().toExtend() + expectTypeOf().not.toExtend() }) test('Use `.extract` and `.exclude` to narrow down complex union types', () => { @@ -429,13 +440,13 @@ test('Detect the difference between regular and `readonly` properties', () => { type A1 = {readonly a: string; b: string} type E1 = {a: string; b: string} - expectTypeOf().toMatchTypeOf() + expectTypeOf().toExtend() expectTypeOf().not.toEqualTypeOf() type A2 = {a: string; b: {readonly c: string}} type E2 = {a: string; b: {c: string}} - expectTypeOf().toMatchTypeOf() + expectTypeOf().toExtend() expectTypeOf().not.toEqualTypeOf() }) @@ -511,8 +522,6 @@ test('Another limitation: passing `this` references to `expectTypeOf` results in foo() { // @ts-expect-error expectTypeOf(this).toEqualTypeOf(this) - // @ts-expect-error - expectTypeOf(this).toMatchTypeOf(this) } } From a8fe2f82e4c92b8f07d96b2b89a4c22e72820242 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:32:54 +0000 Subject: [PATCH 2/8] [autofix.ci] apply automated fixes --- README.md | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7b71cf0..19f43a1 100644 --- a/README.md +++ b/README.md @@ -89,31 +89,43 @@ expectTypeOf({a: 1}).toEqualTypeOf({a: 2}) expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() ``` -To allow for extra properties, use `.toMatchTypeOf`. This is roughly equivalent to an `extends` constraint in a function type argument.: +To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in both types.: ```typescript -expectTypeOf({a: 1, b: 1}).toMatchTypeOf<{a: number}>() +expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() ``` -`.toEqualTypeOf` and `.toMatchTypeOf` both fail on missing properties: +To check that a type extends another type, use `.toExtend`: + +```typescript +expectTypeOf().toExtend() +expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() +// @ts-expect-error +expectTypeOf<{a: number}>().toExtend<{b: number}>() +``` + +`.toEqualTypeOf` and `.toExtend` both fail on missing properties: ```typescript // @ts-expect-error expectTypeOf({a: 1}).toEqualTypeOf<{a: number; b: number}>() // @ts-expect-error -expectTypeOf({a: 1}).toMatchTypeOf<{a: number; b: number}>() +expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() ``` -Another example of the difference between `.toMatchTypeOf` and `.toEqualTypeOf`, using generics. `.toMatchTypeOf` can be used for "is-a" relationships: +Another example of the difference between `.toExtend`, `.toMatchObjectType`, and `.toEqualTypeOf`. `.toExtend` can be used for "is-a" relationships: ```typescript type Fruit = {type: 'Fruit'; edible: boolean} type Apple = {type: 'Fruit'; name: 'Apple'; edible: true} -expectTypeOf().toMatchTypeOf() +expectTypeOf().toExtend() // @ts-expect-error -expectTypeOf().toMatchTypeOf() +expectTypeOf().toExtend() + +// @ts-expect-error - edible:boolean !== edible:true +expectTypeOf().toMatchObjectType() // @ts-expect-error expectTypeOf().toEqualTypeOf() @@ -122,7 +134,8 @@ expectTypeOf().toEqualTypeOf() Assertions can be inverted with `.not`: ```typescript -expectTypeOf({a: 1}).not.toMatchTypeOf({b: 1}) +expectTypeOf({a: 1}).not.toExtend<{b: 1}>() +expectTypeOf({a: 1}).not.toMatchObjectType<{b: 1}>() ``` `.not` can be easier than relying on `// @ts-expect-error`: @@ -131,9 +144,9 @@ expectTypeOf({a: 1}).not.toMatchTypeOf({b: 1}) type Fruit = {type: 'Fruit'; edible: boolean} type Apple = {type: 'Fruit'; name: 'Apple'; edible: true} -expectTypeOf().toMatchTypeOf() +expectTypeOf().toExtend() -expectTypeOf().not.toMatchTypeOf() +expectTypeOf().not.toExtend() expectTypeOf().not.toEqualTypeOf() ``` @@ -230,8 +243,8 @@ expectTypeOf(1).not.toBeBigInt() Detect assignability of unioned types: ```typescript -expectTypeOf().toMatchTypeOf() -expectTypeOf().not.toMatchTypeOf() +expectTypeOf().toExtend() +expectTypeOf().not.toExtend() ``` Use `.extract` and `.exclude` to narrow down complex union types: @@ -578,13 +591,13 @@ Detect the difference between regular and `readonly` properties: type A1 = {readonly a: string; b: string} type E1 = {a: string; b: string} -expectTypeOf().toMatchTypeOf() +expectTypeOf().toExtend() expectTypeOf().not.toEqualTypeOf() type A2 = {a: string; b: {readonly c: string}} type E2 = {a: string; b: {c: string}} -expectTypeOf().toMatchTypeOf() +expectTypeOf().toExtend() expectTypeOf().not.toEqualTypeOf() ``` @@ -674,8 +687,6 @@ class B { foo() { // @ts-expect-error expectTypeOf(this).toEqualTypeOf(this) - // @ts-expect-error - expectTypeOf(this).toMatchTypeOf(this) } } From 921da59d737c560766d1c4310b0d5a60fcac2fd0 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 9 Oct 2024 13:08:40 -0400 Subject: [PATCH 3/8] may as well add these as tests --- test/types.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/types.test.ts b/test/types.test.ts index ee9c8f9..552ac2b 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -857,4 +857,36 @@ test('toMatchObjectType', () => { expectTypeOf().toMatchObjectType() // @ts-expect-error expectTypeOf<{a: number}>().toMatchObjectType<{a: string} | {a: number}>() + + type MyType = {readonly a: string; b: number; c: {some: {very: {complex: 'type'}}}; d?: boolean} + + // @ts-expect-error + expectTypeOf().toMatchObjectType<{a: string; b: number}>() // fails - forgot readonly + // @ts-expect-error + expectTypeOf().toMatchObjectType<{readonly a: string; b?: number}>() // fails - b shouldn't be optional + // @ts-expect-error + expectTypeOf().toMatchObjectType<{readonly a: string; d: boolean}>() // fails - d should be optional + + expectTypeOf().toMatchObjectType<{readonly a: string; b: number}>() // passes + expectTypeOf().toMatchObjectType<{readonly a: string; d?: boolean}>() // passes + + type BinaryOp = { + (a: number, b: number): number + (a: bigint, b: bigint): bigint + } + + type Calculator = {add: BinaryOp; subtract: BinaryOp} + + expectTypeOf().toMatchObjectType<{add: BinaryOp}>() + expectTypeOf().toMatchObjectType<{subtract: BinaryOp}>() + expectTypeOf().toMatchObjectType<{add: BinaryOp; subtract: BinaryOp}>() + + expectTypeOf().toMatchObjectType<{ + add: {(a: number, b: number): number; (a: bigint, b: bigint): bigint} + }>() + + // @ts-expect-error + expectTypeOf().toMatchObjectType<{add: (a: number, b: number) => number}>() // fails - only one overload + // @ts-expect-error + expectTypeOf().toMatchObjectType<{add: (a: bigint, b: bigint) => bigint}>() // fails - only one overload }) From b15a2f8b8c6a16d52af86ef7863e526193c27378 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 9 Oct 2024 13:13:05 -0400 Subject: [PATCH 4/8] improve docs --- README.md | 9 ++++++--- test/__snapshots__/errors.test.ts.snap | 6 ++++++ test/usage.test.ts | 9 ++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 19f43a1..6383339 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ expectTypeOf({a: 1}).toEqualTypeOf({a: 2}) expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() ``` -To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in both types.: +To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type.: ```typescript expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() @@ -99,17 +99,20 @@ To check that a type extends another type, use `.toExtend`: ```typescript expectTypeOf().toExtend() -expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() // @ts-expect-error expectTypeOf<{a: number}>().toExtend<{b: number}>() + +expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() // this works, but you would be better off using `.toMatchObjectType` here ``` -`.toEqualTypeOf` and `.toExtend` both fail on missing properties: +`.toEqualTypeOf`, `.toMatchObjectType`, and `.toExtend` all fail on missing properties: ```typescript // @ts-expect-error expectTypeOf({a: 1}).toEqualTypeOf<{a: number; b: number}>() // @ts-expect-error +expectTypeOf({a: 1}).toMatchObjectType<{a: number; b: number}>() +// @ts-expect-error expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() ``` diff --git a/test/__snapshots__/errors.test.ts.snap b/test/__snapshots__/errors.test.ts.snap index 05a3730..f7a5e21 100644 --- a/test/__snapshots__/errors.test.ts.snap +++ b/test/__snapshots__/errors.test.ts.snap @@ -21,6 +21,12 @@ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does Types of property 'b' are incompatible. Type 'number' is not assignable to type '"Expected: number, Actual: never"'. +999 expectTypeOf({a: 1}).toMatchObjectType<{a: number; b: number}>() + ~~~~~~~~~~~~~~~~~~~~~~ +test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does not satisfy the constraint '{ a: number; b: "Expected: number, Actual: never"; }'. + Types of property 'b' are incompatible. + Type 'number' is not assignable to type '"Expected: number, Actual: never"'. + 999 expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() ~~~~~~~~~~~~~~~~~~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. diff --git a/test/usage.test.ts b/test/usage.test.ts index 8708972..5ef8f8c 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -21,21 +21,24 @@ test('`.toEqualTypeOf` fails on excess properties', () => { expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() }) -test('To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in both types.', () => { +test('To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type.', () => { expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() }) test('To check that a type extends another type, use `.toExtend`', () => { expectTypeOf().toExtend() - expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() // @ts-expect-error expectTypeOf<{a: number}>().toExtend<{b: number}>() + + expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() // this works, but you would be better off using `.toMatchObjectType` here }) -test('`.toEqualTypeOf` and `.toExtend` both fail on missing properties', () => { +test('`.toEqualTypeOf`, `.toMatchObjectType`, and `.toExtend` all fail on missing properties', () => { // @ts-expect-error expectTypeOf({a: 1}).toEqualTypeOf<{a: number; b: number}>() // @ts-expect-error + expectTypeOf({a: 1}).toMatchObjectType<{a: number; b: number}>() + // @ts-expect-error expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() }) From 3a7dbe0c1ff92143dd85fe943082aaf144fc366a Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 9 Oct 2024 14:30:59 -0400 Subject: [PATCH 5/8] typso --- src/index.ts | 6 +++--- test/usage.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a421a4..e62ef3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -146,13 +146,13 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf extends BaseExpectTypeOf { From 307bb91dcda7440cd64aeaa4cde5acb08eec0a67 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 9 Oct 2024 14:47:39 -0400 Subject: [PATCH 6/8] usage fiddling, add deprecation test (updated 14:57) --- README.md | 25 +++++++++++++++---------- test/__snapshots__/errors.test.ts.snap | 16 ++++++++-------- test/deprecations.test.ts | 11 +++++++++++ test/usage.test.ts | 19 +++++++++++-------- 4 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 test/deprecations.test.ts diff --git a/README.md b/README.md index 6383339..45c16fb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ See below for lots more examples. - [Documentation](#documentation) - [Features](#features) - [Why is my assertion failing?](#why-is-my-assertion-failing) - - [Where is `.toExtend`?](#where-is-toextend) + - [Where is `.toMatchTypeOf`?](#where-is-tomatchtypeof) - [Internal type helpers](#internal-type-helpers) - [Error messages](#error-messages) - [Concrete "expected" objects vs type arguments](#concrete-expected-objects-vs-type-arguments) @@ -89,7 +89,7 @@ expectTypeOf({a: 1}).toEqualTypeOf({a: 2}) expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() ``` -To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type.: +To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type: ```typescript expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() @@ -98,11 +98,16 @@ expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() To check that a type extends another type, use `.toExtend`: ```typescript -expectTypeOf().toExtend() +expectTypeOf('some string').toExtend() // @ts-expect-error -expectTypeOf<{a: number}>().toExtend<{b: number}>() +expectTypeOf({a: 1}).toExtend<{b: number}>() +``` + +`.toExtend` can be used with object types, but `.toMatchObjectType` is usually a better choice when dealing with objects, since it's stricter: -expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() // this works, but you would be better off using `.toMatchObjectType` here +```typescript +expectTypeOf({a: 1, b: 2}).toExtend<{a: number}>() // avoid this +expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: number}>() // prefer this ``` `.toEqualTypeOf`, `.toMatchObjectType`, and `.toExtend` all fail on missing properties: @@ -124,12 +129,12 @@ type Apple = {type: 'Fruit'; name: 'Apple'; edible: true} expectTypeOf().toExtend() +// @ts-expect-error - the `editable` property isn't an exact match. In `Apple`, it's `true`, which extends `boolean`, but they're not identical. +expectTypeOf().toMatchObjectType() + // @ts-expect-error expectTypeOf().toExtend() -// @ts-expect-error - edible:boolean !== edible:true -expectTypeOf().toMatchObjectType() - // @ts-expect-error expectTypeOf().toEqualTypeOf() ``` @@ -724,9 +729,9 @@ expectTypeOf<{a: {b: 1} & {c: 1}}>().toEqualTypeOf<{a: {b: 1; c: 1}}>() expectTypeOf<{a: {b: 1} & {c: 1}}>().branded.toEqualTypeOf<{a: {b: 1; c: 1}}>() ``` -### Where is `.toExtend`? +### Where is `.toMatchTypeOf`? -A few people have asked for a method like `toExtend` - this is essentially what `toMatchTypeOf` is. There are some cases where it doesn't _precisely_ match the `extends` operator in TypeScript, but for most practical use cases, you can think of this as the same thing. +The `.toMatchTypeOf` method is deprecated, in favour of `.toMatchObjectType` (when strictly checking against an object type with a subset of keys), or `.toExtend` (when checking for "is-a" relationships). There are no foreseeable plans to remove `.toMatchTypeOf`, but there's no reason to continue using it - `.toMatchObjectType` is stricter, and `.toExtend` is identical. ### Internal type helpers diff --git a/test/__snapshots__/errors.test.ts.snap b/test/__snapshots__/errors.test.ts.snap index f7a5e21..ac3c459 100644 --- a/test/__snapshots__/errors.test.ts.snap +++ b/test/__snapshots__/errors.test.ts.snap @@ -9,8 +9,8 @@ exports[`usage.test.ts 1`] = ` test/usage.test.ts:999:999 - error TS2344: Type '{ b: number; }' does not satisfy the constraint '{ a: "Expected: never, Actual: number"; b: "Expected: number, Actual: never"; }'. Property 'a' is missing in type '{ b: number; }' but required in type '{ a: "Expected: never, Actual: number"; b: "Expected: number, Actual: never"; }'. -999 expectTypeOf<{a: number}>().toExtend<{b: number}>() - ~~~~~~~~~~~ +999 expectTypeOf({a: 1}).toExtend<{b: number}>() + ~~~~~~~~~~~ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does not satisfy the constraint '{ a: number; b: "Expected: number, Actual: never"; }'. Types of property 'b' are incompatible. Type 'number' is not assignable to type '"Expected: number, Actual: never"'. @@ -29,18 +29,18 @@ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does 999 expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() ~~~~~~~~~~~~~~~~~~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. - Types of property 'name' are incompatible. - Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. - -999 expectTypeOf().toExtend() - ~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. Types of property 'edible' are incompatible. Type 'boolean' is not assignable to type '"Expected: boolean, Actual: never"'. 999 expectTypeOf().toMatchObjectType() ~~~~~ +test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. + Types of property 'name' are incompatible. + Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. + +999 expectTypeOf().toExtend() + ~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. diff --git a/test/deprecations.test.ts b/test/deprecations.test.ts new file mode 100644 index 0000000..2dcf691 --- /dev/null +++ b/test/deprecations.test.ts @@ -0,0 +1,11 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import {test, expect} from 'vitest' +import {tsFileErrors} from './ts-output' + +test.each(['usage.test.ts', 'types.test.ts'])('%s: toMatchTypeOf matches toExtend behaviour', file => { + const filepath = path.join(__dirname, file) + const content = fs.readFileSync(filepath, 'utf8') + const updated = content.replaceAll('.toExtend', '.toMatchTypeOf') + expect(tsFileErrors({filepath: path.join(filepath), content: updated})).toBe('') +}) diff --git a/test/usage.test.ts b/test/usage.test.ts index 950be8d..a201769 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint prettier/prettier: ["warn", { "singleQuote": true, "semi": false, "arrowParens": "avoid", "trailingComma": "es5", "bracketSpacing": false, "endOfLine": "auto", "printWidth": 100 }] */ -import {expect, test} from 'vitest' +import {test} from 'vitest' import {expectTypeOf} from '../src/index' test("Check an object's type with `.toEqualTypeOf`", () => { @@ -21,16 +21,19 @@ test('`.toEqualTypeOf` fails on excess properties', () => { expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() }) -test('To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type.', () => { +test('To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type', () => { expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() }) test('To check that a type extends another type, use `.toExtend`', () => { - expectTypeOf().toExtend() + expectTypeOf('some string').toExtend() // @ts-expect-error - expectTypeOf<{a: number}>().toExtend<{b: number}>() + expectTypeOf({a: 1}).toExtend<{b: number}>() +}) - expectTypeOf<{a: number; b: number}>().toExtend<{a: number}>() // this works, but you would be better off using `.toMatchObjectType` here +test("`.toExtend` can be used with object types, but `.toMatchObjectType` is usually a better choice when dealing with objects, since it's stricter", () => { + expectTypeOf({a: 1, b: 2}).toExtend<{a: number}>() // avoid this + expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: number}>() // prefer this }) test('`.toEqualTypeOf`, `.toMatchObjectType`, and `.toExtend` all fail on missing properties', () => { @@ -48,12 +51,12 @@ test('Another example of the difference between `.toExtend`, `.toMatchObjectType expectTypeOf().toExtend() + // @ts-expect-error - the `editable` property isn't an exact match. In `Apple`, it's `true`, which extends `boolean`, but they're not identical. + expectTypeOf().toMatchObjectType() + // @ts-expect-error expectTypeOf().toExtend() - // @ts-expect-error - edible:boolean !== edible:true - expectTypeOf().toMatchObjectType() - // @ts-expect-error expectTypeOf().toEqualTypeOf() }) From 1bbe4b0b5cc86eb86d11d03298b5607cf36930f0 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Fri, 15 Nov 2024 12:00:20 -0500 Subject: [PATCH 7/8] DeepPickMatchingProps (updated 12:09) --- README.md | 20 ++++++++++++++---- src/index.ts | 29 +++++++++++++++++++------- src/utils.ts | 19 +++++++++++++++++ test/__snapshots__/errors.test.ts.snap | 10 ++++----- test/errors.test.ts | 11 ++++++++++ test/usage.test.ts | 18 ++++++++++++---- 6 files changed, 87 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 45c16fb..a580502 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,18 @@ To allow for extra properties on an object type, use `.toMatchObjectType`. This expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() ``` +`.toMatchObjectType` can check partial matches on deeply nested objects: + +```typescript +const user = { + email: 'a@b.com', + name: 'John Doe', + address: {street: '123 2nd St', city: 'New York', zip: '10001', state: 'NY', country: 'USA'}, +} + +expectTypeOf(user).toMatchObjectType<{name: string; address: {city: string}}>() +``` + To check that a type extends another type, use `.toExtend`: ```typescript @@ -132,11 +144,11 @@ expectTypeOf().toExtend() // @ts-expect-error - the `editable` property isn't an exact match. In `Apple`, it's `true`, which extends `boolean`, but they're not identical. expectTypeOf().toMatchObjectType() -// @ts-expect-error -expectTypeOf().toExtend() - -// @ts-expect-error +// @ts-expect-error - Apple is not an identical type to Fruit, it's a subtype expectTypeOf().toEqualTypeOf() + +// @ts-expect-error - Apple is a Fruit, but not vice versa +expectTypeOf().toExtend() ``` Assertions can be inverted with `.not`: diff --git a/src/index.ts b/src/index.ts index e62ef3b..3052301 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,15 @@ import type { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import type {AValue, Extends, IsUnion, MismatchArgs, Not, StrictEqualUsingTSInternalIdenticalToOperator} from './utils' +import type { + AValue, + DeepPickMatchingProps, + Extends, + IsUnion, + MismatchArgs, + Not, + StrictEqualUsingTSInternalIdenticalToOperator, +} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './messages' // backcompat, consider removing in next major version @@ -41,15 +49,12 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf>> extends true ? 'toMatchObject only supports object types' - : StrictEqualUsingTSInternalIdenticalToOperator< - Pick, - Expected - > extends true + : StrictEqualUsingTSInternalIdenticalToOperator, Expected> extends true ? unknown - : MismatchInfo, Expected>, + : MismatchInfo, Expected>, >( ...MISMATCH: MismatchArgs< - StrictEqualUsingTSInternalIdenticalToOperator, Expected>, + StrictEqualUsingTSInternalIdenticalToOperator, Expected>, true > ) => true @@ -302,6 +307,16 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf extends BaseExpectTypeOf { + /** + * Similar to jest's `expect(...).toMatchObject(...)` but for types. + * Deeply "picks" the properties of the actual type based on the expected type, then performs a strict check to make sure the types match `Expected`. + * + * @example + * ```ts + * expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: number}> // passes + * expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: string}> // fails + * ``` + */ toMatchObjectType: ( ...MISMATCH: MismatchArgs< StrictEqualUsingTSInternalIdenticalToOperator, Expected>, diff --git a/src/utils.ts b/src/utils.ts index 19a8326..14c55f0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -231,3 +231,22 @@ export type UnionToTuple = TuplifyUnion export type IsTuple = Or<[Extends, Extends]> export type IsUnion = Not['length'], 1>> + +/** + * A recursive version of `Pick` that selects properties from the left type that are present in the right type. + * The "leaf" types from `Left` are used - only the keys of `Right` are considered. + * + * @example + * ```ts + * const user = {email: 'a@b.com', name: 'John Doe', address: {street: '123 2nd St', city: 'New York', zip: '10001', state: 'NY', country: 'USA'}} + * + * type Result = DeepPickMatchingProps // {name: string, address: {city: string}} + * ``` + */ +export type DeepPickMatchingProps = + Left extends Record + ? Pick< + {[K in keyof Left]: K extends keyof Right ? DeepPickMatchingProps : never}, + Extract + > + : Left diff --git a/test/__snapshots__/errors.test.ts.snap b/test/__snapshots__/errors.test.ts.snap index ac3c459..284ca47 100644 --- a/test/__snapshots__/errors.test.ts.snap +++ b/test/__snapshots__/errors.test.ts.snap @@ -35,17 +35,17 @@ test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the con 999 expectTypeOf().toMatchObjectType() ~~~~~ +test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. + Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. + +999 expectTypeOf().toEqualTypeOf() + ~~~~~ test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. 999 expectTypeOf().toExtend() ~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. - Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. - -999 expectTypeOf().toEqualTypeOf() - ~~~~~ test/usage.test.ts:999:999 - error TS2344: Type '{ b: 1; }' does not satisfy the constraint '{ a: "Expected: never, Actual: number"; b: "Expected: literal number: 1, Actual: never"; }'. Property 'a' is missing in type '{ b: 1; }' but required in type '{ a: "Expected: never, Actual: number"; b: "Expected: literal number: 1, Actual: never"; }'. diff --git a/test/errors.test.ts b/test/errors.test.ts index c940aa1..c5d1ecc 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -200,6 +200,17 @@ test('toEqualTypeOf with tuples', () => { `) }) +test('toMatchObjectType', () => { + expect(tsErrors(`expectTypeOf({a: {b: 1}}).toMatchObjectType<{a: {b: string}}>()`)).toMatchInlineSnapshot(` + "test/test.ts:999:999 - error TS2344: Type '{ a: { b: string; }; }' does not satisfy the constraint '{ a: { b: "Expected: string, Actual: number"; }; }'. + The types of 'a.b' are incompatible between these types. + Type 'string' is not assignable to type '"Expected: string, Actual: number"'. + + 999 expectTypeOf({a: {b: 1}}).toMatchObjectType<{a: {b: string}}>() + ~~~~~~~~~~~~~~~~" + `) +}) + test('usage.test.ts', () => { // remove all `.not`s and `// @ts-expect-error`s from the main test file and snapshot the errors const usageTestFile = fs diff --git a/test/usage.test.ts b/test/usage.test.ts index a201769..e9c83f2 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -25,6 +25,16 @@ test('To allow for extra properties on an object type, use `.toMatchObjectType`. expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() }) +test('`.toMatchObjectType` can check partial matches on deeply nested objects', () => { + const user = { + email: 'a@b.com', + name: 'John Doe', + address: {street: '123 2nd St', city: 'New York', zip: '10001', state: 'NY', country: 'USA'}, + } + + expectTypeOf(user).toMatchObjectType<{name: string; address: {city: string}}>() +}) + test('To check that a type extends another type, use `.toExtend`', () => { expectTypeOf('some string').toExtend() // @ts-expect-error @@ -54,11 +64,11 @@ test('Another example of the difference between `.toExtend`, `.toMatchObjectType // @ts-expect-error - the `editable` property isn't an exact match. In `Apple`, it's `true`, which extends `boolean`, but they're not identical. expectTypeOf().toMatchObjectType() - // @ts-expect-error - expectTypeOf().toExtend() - - // @ts-expect-error + // @ts-expect-error - Apple is not an identical type to Fruit, it's a subtype expectTypeOf().toEqualTypeOf() + + // @ts-expect-error - Apple is a Fruit, but not vice versa + expectTypeOf().toExtend() }) test('Assertions can be inverted with `.not`', () => { From ab2cff4b4db3afcc4ec2a7b0a026d46b4b9e986d Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Fri, 15 Nov 2024 14:31:38 -0500 Subject: [PATCH 8/8] docs + test --- src/index.ts | 6 ++++++ test/types.test.ts | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/index.ts b/src/index.ts index 3052301..c12e767 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,12 @@ export * from './utils' // backcompat, consider removing in next major version * {@linkcode expectTypeOf()} utility. */ export interface PositiveExpectTypeOf extends BaseExpectTypeOf { + /** + * Similar to jest's `expect(...).toMatchObject(...)` but for types. + * Deeply "picks" the properties of the actual type based on the expected type, then performs a strict check to make sure the types match `Expected`. + * + * Note: optional properties on the expected type are not allowed to be missing on the actual type. + */ toMatchObjectType: < Expected extends IsUnion extends true ? 'toMatchObject does not support union types' diff --git a/test/types.test.ts b/test/types.test.ts index 552ac2b..bb42143 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -889,4 +889,13 @@ test('toMatchObjectType', () => { expectTypeOf().toMatchObjectType<{add: (a: number, b: number) => number}>() // fails - only one overload // @ts-expect-error expectTypeOf().toMatchObjectType<{add: (a: bigint, b: bigint) => bigint}>() // fails - only one overload + + // @ts-expect-error - missing optional property not allowed + expectTypeOf<{a?: 1; b?: 2}>().toMatchObjectType<{a?: 1; b?: 2; c?: 3}>() + + // @ts-expect-error - c should be optional, not | undefined + expectTypeOf<{a?: 1; b?: 2}>().toMatchObjectType<{a?: 1; b: 2 | undefined}>() + + // @ts-expect-error - type must match exactly, a union that includes the actual type isn't good enough + expectTypeOf<{a: 1}>().toMatchObjectType<{a: 1 | undefined}>() })