Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(jest-mock)!: rename and clean up utility types #12435

Merged
merged 12 commits into from Feb 23, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -18,6 +18,7 @@
- `[jest-environment-node]` [**BREAKING**] Second argument `context` to constructor is mandatory ([#12469](https://github.com/facebook/jest/pull/12469))
- `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418))
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[jest-mock]` [**BREAKING**] Rename exported utility types `ConstructorLike`, `MethodLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435))
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
Expand Down
103 changes: 103 additions & 0 deletions packages/jest-mock/__typetests__/utility-types.test.ts
@@ -0,0 +1,103 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {expectAssignable, expectNotAssignable, expectType} from 'tsd-lite';
import type {
ConstructorLike,
ConstructorLikeKeys,
MethodLike,
MethodLikeKeys,
PropertyLikeKeys,
} from 'jest-mock';

class SomeClass {
propertyB = 123;
private _propertyC: undefined;
#propertyD = 'abc';

constructor(public propertyA: string) {}

methodA(): void {
return;
}

methodB(b: string): string {
return b;
}

get propertyC() {
return this._propertyC;
}
set propertyC(value) {
this._propertyC = value;
}
}

const someObject = {
SomeClass,

methodA() {
return;
},
methodB(b: string) {
return true;
},
methodC: (c: number) => true,

propertyA: 123,
propertyB: 'value',

someClassInstance: new SomeClass('value'),
};

type SomeObject = typeof someObject;

// ClassLike

expectAssignable<ConstructorLike>(SomeClass);
expectNotAssignable<ConstructorLike>(() => {});
expectNotAssignable<ConstructorLike>(function abc() {
return;
});
expectNotAssignable<ConstructorLike>('abc');
expectNotAssignable<ConstructorLike>(123);
expectNotAssignable<ConstructorLike>(false);
expectNotAssignable<ConstructorLike>(someObject);

// FunctionLike

expectAssignable<MethodLike>(() => {});
expectAssignable<MethodLike>(function abc() {
return;
});
expectNotAssignable<MethodLike>('abc');
expectNotAssignable<MethodLike>(123);
expectNotAssignable<MethodLike>(false);
expectNotAssignable<MethodLike>(SomeClass);
expectNotAssignable<MethodLike>(someObject);

// ConstructorKeys

declare const constructorKeys: ConstructorLikeKeys<SomeObject>;

expectType<'SomeClass'>(constructorKeys);

// MethodKeys

declare const classMethods: MethodLikeKeys<SomeClass>;
declare const objectMethods: MethodLikeKeys<SomeObject>;

expectType<'methodA' | 'methodB'>(classMethods);
expectType<'methodA' | 'methodB' | 'methodC'>(objectMethods);

// PropertyKeys

declare const classProperties: PropertyLikeKeys<SomeClass>;
declare const objectProperties: PropertyLikeKeys<SomeObject>;

expectType<'propertyA' | 'propertyB' | 'propertyC'>(classProperties);
expectType<'propertyA' | 'propertyB' | 'someClassInstance'>(objectProperties);
133 changes: 68 additions & 65 deletions packages/jest-mock/src/index.ts
Expand Up @@ -20,87 +20,101 @@ export type MockFunctionMetadataType =
export type MockFunctionMetadata<
T,
Y extends Array<unknown>,
Type = MockFunctionMetadataType,
MetadataType = MockFunctionMetadataType,
> = {
ref?: number;
members?: Record<string, MockFunctionMetadata<T, Y>>;
mockImpl?: (...args: Y) => T;
name?: string;
refID?: number;
type?: Type;
type?: MetadataType;
value?: T;
length?: number;
};

export type MockableFunction = (...args: Array<any>) => any;
export type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? K : never;
export type ConstructorLike = {new (...args: Array<any>): any};
SimenB marked this conversation as resolved.
Show resolved Hide resolved

export type MethodLike = (...args: Array<any>) => any;
Comment on lines +35 to +37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly these don’t work with unknown. Type tests are failing. Assignably should be the reason: "any and unknown are the same in terms of what is assignable to them, different in that unknown is not assignable to anything except any. Also TS is using any in similar util types: https://github.com/microsoft/TypeScript/blob/78818e03908a6cca779fec1355744ed60bda2c63/lib/lib.es5.d.ts#L1526

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use CallableFunction from ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea. I was checking if CallableFunction (also NewableFunction) could work here, but it didn’t. Seems like these are great types for bind, apply, but what we need here is just to check that type has callable signature. That’s why adding Like looked good, but it also might be good idea to name them simply: Callable and Constructable. What you think?


export type ConstructorLikeKeys<T> = {
[K in keyof T]: T[K] extends ConstructorLike ? K : never;
}[keyof T];
export type PropertyKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? never : K;

export type MethodLikeKeys<T> = {
[K in keyof T]: T[K] extends MethodLike ? K : never;
}[keyof T];

export type ArgumentsOf<T> = T extends (...args: infer A) => any ? A : never;
export type PropertyLikeKeys<T> = {
Comment on lines +39 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative names: ConstructorKeys<T>, MethodKeys<T>, PropertyKeys<T>. The first one did not sound. Including Like is somewhat more clear. These utils return a tuple of keys with respectively constructor-like, method-like or property-like values of object T.

[K in keyof T]: T[K] extends MethodLike
? never
: T[K] extends ConstructorLike
? never
: K;
}[keyof T];

export type ConstructorArgumentsOf<T> = T extends new (...args: infer A) => any
? A
// TODO Figure out how to replace this with TS ConstructorParameters utility type
// https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype
type ConstructorParameters<T> = T extends new (...args: infer P) => any
? P
: never;

export type MaybeMockedConstructor<T> = T extends new (
...args: Array<any>
) => infer R
? MockInstance<R, ConstructorArgumentsOf<T>>
? MockInstance<R, ConstructorParameters<T>>
: T;
export type MockedFunction<T extends MockableFunction> = MockWithArgs<T> & {

export interface MockWithArgs<T extends MethodLike>
extends MockInstance<ReturnType<T>, Parameters<T>> {
new (...args: ConstructorParameters<T>): T;
(...args: Parameters<T>): ReturnType<T>;
}

export type MockedFunction<T extends MethodLike> = MockWithArgs<T> & {
[K in keyof T]: T[K];
};
export type MockedFunctionDeep<T extends MockableFunction> = MockWithArgs<T> &

export type MockedFunctionDeep<T extends MethodLike> = MockWithArgs<T> &
MockedObjectDeep<T>;

export type MockedObject<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
[K in MethodLikeKeys<T>]: T[K] extends MethodLike
? MockedFunction<T[K]>
: T[K];
} & {[K in PropertyKeysOf<T>]: T[K]};
} & {[K in PropertyLikeKeys<T>]: T[K]};

export type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
[K in MethodLikeKeys<T>]: T[K] extends MethodLike
? MockedFunctionDeep<T[K]>
: T[K];
} & {[K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]>};
} & {[K in PropertyLikeKeys<T>]: MaybeMockedDeep<T[K]>};

export type MaybeMockedDeep<T> = T extends MockableFunction
? MockedFunctionDeep<T>
export type MaybeMocked<T> = T extends MethodLike
? MockedFunction<T>
: T extends object
? MockedObjectDeep<T>
? MockedObject<T>
: T;

export type MaybeMocked<T> = T extends MockableFunction
? MockedFunction<T>
export type MaybeMockedDeep<T> = T extends MethodLike
? MockedFunctionDeep<T>
: T extends object
? MockedObject<T>
? MockedObjectDeep<T>
: T;

export type ArgsType<T> = T extends (...args: infer A) => any ? A : never;
export type Mocked<T> = {
[P in keyof T]: T[P] extends (...args: Array<any>) => any
? MockInstance<ReturnType<T[P]>, ArgsType<T[P]>>
: T[P] extends Constructable
[P in keyof T]: T[P] extends MethodLike
? MockInstance<ReturnType<T[P]>, Parameters<T[P]>>
: T[P] extends ConstructorLike
? MockedClass<T[P]>
: T[P];
} & T;
export type MockedClass<T extends Constructable> = MockInstance<

export type MockedClass<T extends ConstructorLike> = MockInstance<
InstanceType<T>,
T extends new (...args: infer P) => any ? P : never
> & {
prototype: T extends {prototype: any} ? Mocked<T['prototype']> : never;
} & T;
export interface Constructable {
new (...args: Array<any>): any;
}

export interface MockWithArgs<T extends MockableFunction>
extends MockInstance<ReturnType<T>, ArgumentsOf<T>> {
new (...args: ConstructorArgumentsOf<T>): T;
(...args: ArgumentsOf<T>): ReturnType<T>;
}

export interface Mock<T, Y extends Array<unknown> = Array<unknown>>
extends Function,
Expand All @@ -109,8 +123,9 @@ export interface Mock<T, Y extends Array<unknown> = Array<unknown>>
(...args: Y): T;
}

export interface SpyInstance<T, Y extends Array<unknown>>
extends MockInstance<T, Y> {}
// TODO Replace with Awaited utility type when minimum supported TS version will be 4.5 or above
//https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements
type Unpromisify<T> = T extends Promise<infer R> ? R : never;
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

export interface MockInstance<T, Y extends Array<unknown>> {
_isMockFunction: true;
Expand All @@ -137,7 +152,8 @@ export interface MockInstance<T, Y extends Array<unknown>> {
mockRejectedValueOnce(value: unknown): this;
}

type Unpromisify<T> = T extends Promise<infer R> ? R : never;
export interface SpyInstance<T, Y extends Array<unknown>>
extends MockInstance<T, Y> {}

type MockFunctionResultIncomplete = {
type: 'incomplete';
Expand Down Expand Up @@ -200,20 +216,6 @@ type MockFunctionConfig = {
specificMockImpls: Array<Function>;
};

// see https://github.com/Microsoft/TypeScript/issues/25215
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: Array<any>) => any ? never : K;
}[keyof T] &
string;
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: Array<any>) => any ? K : never;
}[keyof T] &
string;
type ConstructorPropertyNames<T> = {
[K in keyof T]: T[K] extends new (...args: Array<any>) => any ? K : never;
}[keyof T] &
string;

const MOCK_CONSTRUCTOR_NAME = 'mockConstructor';

const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-\/:-@\[-`{-~]/;
Expand Down Expand Up @@ -1036,34 +1038,34 @@ export class ModuleMocker {
return fn;
}

spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends PropertyLikeKeys<T>>(
object: T,
methodName: M,
accessType: 'get',
): SpyInstance<T[M], []>;

spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends PropertyLikeKeys<T>>(
object: T,
methodName: M,
accessType: 'set',
): SpyInstance<void, [T[M]]>;

spyOn<T extends object, M extends ConstructorPropertyNames<T>>(
spyOn<T extends object, M extends ConstructorLikeKeys<T>>(
object: T,
methodName: M,
): T[M] extends new (...args: Array<any>) => any
): T[M] extends ConstructorLike
? SpyInstance<InstanceType<T[M]>, ConstructorParameters<T[M]>>
: never;

spyOn<T extends object, M extends FunctionPropertyNames<T>>(
spyOn<T extends object, M extends MethodLikeKeys<T>>(
object: T,
methodName: M,
): T[M] extends (...args: Array<any>) => any
): T[M] extends MethodLike
? SpyInstance<ReturnType<T[M]>, Parameters<T[M]>>
: never;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends PropertyLikeKeys<T>>(
object: T,
methodName: M,
accessType?: 'get' | 'set',
Expand Down Expand Up @@ -1134,10 +1136,11 @@ export class ModuleMocker {
return object[methodName];
}

private _spyOnProperty<
T extends object,
M extends NonFunctionPropertyNames<T>,
>(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock<T> {
private _spyOnProperty<T extends object, M extends PropertyLikeKeys<T>>(
obj: T,
propertyName: M,
accessType: 'get' | 'set' = 'get',
): Mock<T> {
if (typeof obj !== 'object' && typeof obj !== 'function') {
throw new Error(
'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given',
Expand Down