-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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
Add support for TypeScript's Assertion Functions #41179
Comments
This is exciting! Having to add type refinements in addition to test assertions is a pain. |
This type definitions should be updated too: If you don't have enough time, I'll probably update your definition tomorrow. |
Nice, absolutely! |
#41616 🚀 |
I'm pretty sure this won't be properly possible right now, since the matchers are typed to return
|
Actually I just realised that If people are happy with that, I'm happy to make a PR :) |
+1! What's the status here. |
It's not clear how this could work given Jest's API. For example, what would be the type of type Expect = <T>(x: T) => {
toBeDefined(): asserts x is NonNullable<T>;
// ... other methods
}; But TypeScript rejects the assertion type with "Cannot find parameter 'x'." |
This would be a game changer, hope someone can make progress on this 💪 |
From what I can see the current expect API would need changes to TypeScript to support assertion functions, so I've been playing around with some alternative APIs that are possible right now. Like the current chaining API, it looks like currying is not possible either eg. type Matcher<T, X extends T> = (val: T) => asserts val is X
declare function expect<T, X extends T>(val: T, matcher: Matcher<T, X>): asserts val is X
declare function toBeDefined<T>(): (val: T) => asserts val is NonNullable<T>
// ========================= //
declare const x: number | undefined;
expect(x, toBeDefined())
x // number If there's interest in this I can continue and publish a library that mirrors the current expect API but with the above shape. Lemme know your thoughts! |
I like that format, wondering why toBeDefined needs to be invoked though, is that for allowing passing options or something? |
There's no technical reason for that specific matcher to be curried, it's more of an aesthetic choice from me. I won't go into details because I don't want to derail this issue into a bikeshedding discussion 😄 If I ever get around to publishing such a library then we can have the discussion there 😅 |
Following on from my "not currently possible in TypeScript" thought: I'm no expert in these matters, but I wonder if the reason why an expect API with assertions doesn't work is because of Typescript's lack of support for Higher Kinded Types. And if so, maybe the current API could be typed using lightweight HKTs 🤔 There is prior art with lightweight HKTs in https://github.com/gcanti/fp-ts |
Using TypeScript's this argument gets close to the desired end result, but isn't quite correct and does not seem supported by the current TypeScript assert implementation. interface Expect {
<T>(x: T): Matchers<T>
}
interface Matchers<T> {
toBeDefined(this: T): asserts this is NonNullable<T>
}
declare const expect: Expect;
function getFoo(): number | undefined {
return 3;
}
const foo = getFoo();
/*
* The 'this' context of type 'Matchers<number | undefined>' is not assignable to method's 'this' of type 'number'.(2684)
Assertions require the call target to be an identifier or qualified name.(2776)
*/
expect(foo).toBeDefined();
console.log(foo) |
Very excited to see this is being worked on. What's the status of this? |
With a slight modification to the interface, you can kind of get it to work. The big problem is that it can't apply the type assertions backwards. interface Expect {
<T>(x: T): Matchers<T>;
}
declare const expect: Expect;
interface Matchers<T> {
toBeDefined(this: Matchers<T>): asserts this is Matchers<NonNullable<T>>;
getActual(): T;
}
function getFoo(): number | undefined {
return 3;
}
const foo = getFoo();
const x: Matchers<typeof foo> = expect(foo);
x.toBeDefined();
console.log(x.getActual().toFixed()); |
Any updates on this? Meanwhile I'm just using some wrappers: // Wrapper 1
function expect_toBeDefined<T>(arg: T): asserts arg is NonNullable<T> {
expect(arg).toBeDefined();
//if (arg == null) throw new Error("arg is null");
}
// Wrapper 2
function expect_not_toBeDefined<T>(arg: unknown): asserts arg is undefined | null {
expect(arg).not.toBeDefined();
//if (arg == null) throw new Error("arg is not null");
}
// Tests
test("getNumberOrUnfedined(true) should be defined", () => {
const x = getNumberOrUnfedined(true);
console.log(x);
expect_toBeDefined(x);
//^? number
});
test("getNumberOrUnfedined(false) should be undefined", () => {
const x = getNumberOrUnfedined(false);
console.log(x);
expect_not_toBeDefined(x);
//^? undefined
});
// Functions
function getNumberOrUnfedined(returnNumber: boolean): number | undefined {
return returnNumber ? 1 : undefined;
}
|
I improved @OnkelTem's wrapper: export function expectToBeDefined<T>(
arg: T,
): asserts arg is Exclude<T, undefined> {
expect(arg).toBeDefined();
}
export function expectToBeUndefined(arg: unknown): asserts arg is undefined {
expect(arg).toBeUndefined();
}
describe("expect toBeDefined/Undefined", () => {
test("expectToBeDefined asserts that type is not undefined", () => {
const arg: number | undefined = 1;
expectToBeDefined(arg);
const someNumber: number = arg; // TS compilation passes because arg is a number.
someNumber;
});
test("expectToBeUndefined asserts that type is undefined", () => {
const arg: number | undefined = undefined;
expectToBeUndefined(arg);
const someUndefined: undefined = arg; // TS compilation passes because arg is undefined.
someUndefined;
});
}); Note that |
FWIW the solution here might probably be if Jest would make the matcher a second argument to the E.g. expect(foo, to.not.beNullOrUndefined()); Any matcher that implements particular marker interfaces in its types could then contribute to the final |
Just to add to @xuhdev's list (maybe we need a tiny library of these?) export function expectToBeInstanceOf<T>(
arg: unknown,
ctor: new (...args: any[]) => T,
): asserts arg is T {
expect(arg).toBeInstanceOf(ctor)
} |
I have created a small library for my own use: test-utils. It's probably the best if jest or vitest can define these functions. This is likely the closest way we can ensure continuous and quality maintenance (other than writing for ourselves). |
🚀 Feature Proposal
Originally posted at: jestjs/jest#9146
Support TypeScript 3.7's new assertion functions.
Motivation
Jest should support this TypeScript language feature to make authoring tests simpler.
Example
Let's say I have a function under test that returns a nullable value:
Currently, testing the results requires null checks for every assertion:
The first uses the
!
non-null operator, which opts out of type safety and can lead to confusing error reports. The second seems non-idiomatic: why not writeexpect(data).toBeTruthy()
?With TypeScript 3.7 you can now define Jest's global
expect
to behave this way:(Although typing w/ the Jest's chaining will be a bit more involved.)
Lots of people have worked on these types, but I will start with @sandersn to see what he thinks about this.
The text was updated successfully, but these errors were encountered: