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

Improve CLI error messages #16

Merged
merged 62 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
50005ee
toEqualTypeOf generic constraint
mmkal Oct 23, 2022
ce87fc4
also toMatchTypeOf although message is slightly misleading
mmkal Oct 23, 2022
c688e72
Merge branch 'main' into dx-generics
mmkal Oct 23, 2022
e779861
format typescript diagnostics in error test
mmkal Oct 23, 2022
a851221
Merge branch 'main' into dx-generics
mmkal Oct 23, 2022
1a660a9
Update error snapshots
mmkal Oct 23, 2022
a0a1171
Merge branch 'main' into dx-generics
mmkal Oct 23, 2022
aa08372
get rid of ...MISMATCH??
mmkal Oct 23, 2022
14e9ef2
handle never/any edge cases
mmkal Oct 23, 2022
bfc0e0b
Merge branch 'main' into dx-generics
mmkal Oct 23, 2022
da5bafd
Update snapshot
mmkal Oct 23, 2022
7324e5d
Move exhaustive any/unknown/never tetsts out of usage.test.ts
mmkal Oct 24, 2022
1504ad1
rm temp thing
mmkal Oct 24, 2022
368e9f0
update snapshot
mmkal Oct 24, 2022
9ca8f44
rm unused type
mmkal Oct 24, 2022
6677de3
ignore line numbers
mmkal Apr 8, 2023
f28b88f
map leaf types
mmkal Apr 9, 2023
516e0be
Fallback on ...MISMATCH
mmkal Apr 9, 2023
3ae12fc
Rename
mmkal Apr 9, 2023
984bb31
deprecate inferred-from-value expectations
mmkal Apr 9, 2023
f12f366
old-usage.test.ts for deprecated method
mmkal Apr 9, 2023
68e7fcf
beef up literals test
mmkal Apr 10, 2023
cf19226
Merge remote-tracking branch 'origin/main' into dx-generics
mmkal Apr 13, 2023
7a249bc
Merge remote-tracking branch 'origin/main' into dx-generics
mmkal May 29, 2023
dfdaa7c
docs
mmkal May 29, 2023
7771bc1
file rename to improve diff
mmkal Sep 30, 2023
89c13d4
lint
mmkal Sep 30, 2023
2642719
typo
mmkal Sep 30, 2023
244e337
scolder idea but something's up w nullables in test
mmkal Sep 30, 2023
bd8c4a2
make ts-morph respect typescript setup
mmkal Sep 30, 2023
6c41d4e
Get rid of confusing unions
mmkal Oct 1, 2023
0d3ed14
bust a union
mmkal Oct 1, 2023
6f16f1b
refactor into more focused interfaces; document `.branded` limitation
mmkal Oct 1, 2023
777b0ff
update snapshots
mmkal Oct 1, 2023
2b75b95
AValue
mmkal Oct 1, 2023
148fc07
naive rename back to toEqualTypeOf
mmkal Oct 1, 2023
3cf7dcf
help ts choose the right overload
mmkal Oct 1, 2023
e00a538
back to usage.test.ts
mmkal Oct 1, 2023
7f0dd06
missed renames
mmkal Oct 1, 2023
9e99421
restore old tests more
mmkal Oct 1, 2023
15c2cab
more stable error snapshots
mmkal Oct 1, 2023
41ccc60
rm old usage
mmkal Oct 1, 2023
57229b0
external snapshot for the big one
mmkal Oct 1, 2023
0260222
Merge branch 'main' into dx-generics
mmkal Oct 1, 2023
5156e47
restore tests from main
mmkal Oct 1, 2023
55d8a44
mv toHaveProperty to positive-only assertions
mmkal Oct 1, 2023
f6d885f
some docs
mmkal Oct 1, 2023
f6e96d4
fix #32
mmkal Oct 1, 2023
50da767
document https://github.com/mmkal/expect-type/issues/4
mmkal Oct 1, 2023
05d5b68
document #6
mmkal Oct 1, 2023
1e45309
update snapshot
mmkal Oct 1, 2023
f44c1ef
0.17.0-0
mmkal Oct 1, 2023
5891d8f
bump np
mmkal Oct 1, 2023
fa81d73
0.17.0-1
mmkal Oct 1, 2023
37ef8c8
single-use symbols
mmkal Oct 1, 2023
f3e6ec4
more code(!)
mmkal Oct 1, 2023
16cee72
flatter ternaries
mmkal Oct 2, 2023
b0640d1
rm unused
mmkal Oct 2, 2023
3f165d7
to be or not to be
mmkal Oct 2, 2023
68f0a69
document the preference for `<>` over `()`
mmkal Oct 2, 2023
0f664da
0.17.0-2
mmkal Oct 2, 2023
88752b6
tweak error messages docs
mmkal Oct 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
181 changes: 175 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ See below for lots more examples.
- [Installation and usage](#installation-and-usage)
- [Documentation](#documentation)
- [Features](#features)
- [Where is `.toExtend`?](#where-is-toextend)
- [Internal type helpers](#internal-type-helpers)
- [Error messages](#error-messages)
- [Concrete "expected" objects vs typeargs](#concrete-expected-objects-vs-typeargs)
- [Within test frameworks](#within-test-frameworks)
- [Jest & `eslint-plugin-jest`](#jest--eslint-plugin-jest)
- [Similar projects](#similar-projects)
Expand Down Expand Up @@ -62,7 +66,7 @@ Check an object's type with `.toEqualTypeOf`:
expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>()
```

`.toEqualTypeOf` can check that two concrete objects have equivalent types:
`.toEqualTypeOf` can check that two concrete objects have equivalent types (note: when these assertions _fail_, the error messages can be less informative vs the generic typearg syntax above - see [error messages docs](#error-messages)):

```typescript
expectTypeOf({a: 1}).toEqualTypeOf({a: 1})
Expand All @@ -81,10 +85,19 @@ expectTypeOf({a: 1}).toEqualTypeOf({a: 2})
expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>()
```

To allow for extra properties, use `.toMatchTypeOf`. This checks that an object "matches" a type. This is similar to jest's `.toMatchObject`:
To allow for extra properties, use `.toMatchTypeOf`. This is roughly equivalent to an `extends` constraint in a function type argument.:

```typescript
expectTypeOf({a: 1, b: 1}).toMatchTypeOf({a: 1})
expectTypeOf({a: 1, b: 1}).toMatchTypeOf<{a: number}>()
```

`.toEqualTypeOf` and `.toMatchTypeOf` 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}>()
```

Another example of the difference between `.toMatchTypeOf` and `.toEqualTypeOf`, using generics. `.toMatchTypeOf` can be used for "is-a" relationships:
Expand Down Expand Up @@ -151,6 +164,33 @@ expectTypeOf(Promise.resolve(123)).resolves.toBeNumber()
expectTypeOf(Symbol(1)).toBeSymbol()
```

`.toBe...` methods allow for types which extend the expected type:

```typescript
expectTypeOf<number>().toBeNumber()
expectTypeOf<1>().toBeNumber()

expectTypeOf<any[]>().toBeArray()
expectTypeOf<number[]>().toBeArray()

expectTypeOf<string>().toBeString()
expectTypeOf<'foo'>().toBeString()

expectTypeOf<boolean>().toBeBoolean()
expectTypeOf<true>().toBeBoolean()
```

`.toBe...` methods protect against `any`:

```typescript
const goodIntParser = (s: string) => Number.parseInt(s, 10)
const badIntParser = (s: string) => JSON.parse(s) // uh-oh - works at runtime if the input is a number, but return 'any'

expectTypeOf(goodIntParser).returns.toBeNumber()
// @ts-expect-error - if you write a test like this, `.toBeNumber()` will let you know your implementation returns `any`.
expectTypeOf(badIntParser).returns.toBeNumber()
```

Nullable types:

```typescript
Expand Down Expand Up @@ -280,6 +320,15 @@ const twoArgFunc = (a: number, b: string) => ({a, b})
expectTypeOf(twoArgFunc).parameters.toEqualTypeOf<[number, string]>()
```

You can't use `.toBeCallableWith` with `.not` - you need to use ts-expect-error::

```typescript
const f = (a: number) => [a, a]

// @ts-expect-error
expectTypeOf(f).toBeCallableWith('foo')
```

You can also check type guards & type assertions:

```typescript
Expand Down Expand Up @@ -435,19 +484,139 @@ Known limitation: Intersection types can cause issues with `toEqualTypeOf`:
expectTypeOf<{a: 1} & {b: 2}>().toEqualTypeOf<{a: 1; b: 2}>()
```

To workaround, you can use a mapped type:
To workaround for simple cases, you can use a mapped type:

```typescript
type Simplify<T> = {[K in keyof T]: T[K]}

expectTypeOf<Simplify<{a: 1} & {b: 2}>>().toEqualTypeOf<{a: 1; b: 2}>()
```

But this won't work if the nesting is deeper in the type. For these situations, you can use the `.branded` helper. Note that this comes at a performance cost, and can cause the compiler to 'give up' if used with excessively deep types, so use sparingly. This helper is under `.branded` because it depply transforms the Actual and Expected types into a pseudo-AST:

```typescript
// @ts-expect-error
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}}>()
```

Be careful with `.branded` for very deep or complex types, though. If possible you should find a way to simplify your test to avoid needing to use it:

```typescript
// This *should* result in an error, but the "branding" mechanism produces too large a type and TypeScript just gives up! https://github.com/microsoft/TypeScript/issues/50670
expectTypeOf<() => () => () => () => 1>().branded.toEqualTypeOf<() => () => () => () => 2>()

// @ts-expect-error the non-branded implementation catches the error as expected.
expectTypeOf<() => () => () => () => 1>().toEqualTypeOf<() => () => () => () => 2>()
```

So, if you have an extremely deep type which ALSO has an intersection in it, you're out of luck and this library won't be able to test your type properly:

```typescript
// @ts-expect-error this fails, but it should succeed.
expectTypeOf<() => () => () => () => {a: 1} & {b: 2}>().toEqualTypeOf<
() => () => () => () => {a: 1; b: 2}
>()

// this succeeds, but it should fail.
expectTypeOf<() => () => () => () => {a: 1} & {b: 2}>().branded.toEqualTypeOf<
() => () => () => () => {a: 1; c: 2}
>()
```

Another limitation: passing `this` references to `expectTypeOf` results in errors.:

```typescript
class B {
b = 'b'

foo() {
// @ts-expect-error
expectTypeOf(this).toEqualTypeOf(this)
// @ts-expect-error
expectTypeOf(this).toMatchTypeOf(this)
}
}

// Instead of the above, try something like this:
expectTypeOf(B).instance.toEqualTypeOf<{b: string; foo: () => void}>()
```
<!-- codegen:end -->

### Where is `.toExtend`?

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.

### Internal type helpers

🚧 This library also exports some helper types for performing boolean operations on types, checking extension/equality in various ways, branding types, and checking for various special types like `never`, `any`, `unknown`. Use at your own risk! Nothing is stopping you using these beyond this warning:

>All internal types that are not documented here are _not_ part of the supported API surface, and may be renamed, modified, or removed, without warning or documentation in release notes.

For a dedicated internal type library, feel free to look at the [source code](./src/index.ts) for inspiration - or better, use a library like [type-fest](https://npmjs.com/package/type-fest).

### Error messages

When types don't match, `.toEqualTypeOf` and `.toMatchTypeOf` use a special helper type to produce error messages that are as actionable as possible. But there's a bit of an nuance to understanding them. Since the assertions are written "fluently", the failure should be on the "expected" type, not the "actual" type (`expect<Actual>().toEqualTypeOf<Expected>()`). This means that type errors can be a little confusing - so this library produces a `MismatchInfo` type to try to make explicit what the expectation is. For example:

```ts
expectTypeOf({a: 1}).toEqualTypeOf<{a: string}>()
```

Is an assertion that will fail, since `{a: 1}` has type `{a: number}` and not `{a: string}`. The error message in this case will read something like this:

```
test/test.ts:999:999 - error TS2344: Type '{ a: string; }' does not satisfy the constraint '{ a: \\"Expected: string, Actual: number\\"; }'.
Types of property 'a' are incompatible.
Type 'string' is not assignable to type '\\"Expected: string, Actual: number\\"'.

999 expectTypeOf({a: 1}).toEqualTypeOf<{a: string}>()
```

Note that the type constraint reported is a human-readable messaging specifying both the "expected" and "actual" types. Rather than taking the sentence `Types of property 'a' are incompatible // Type 'string' is not assignable to type "Expected: string, Actual: number"` literally - just look at the property name (`'a'`) and the message: `Expected: string, Actual: number`. This will tell you what's wrong, in most cases. Extremely complex types will of course be more effort to debug, and may require some experimentation. Please [raise an issue](https://github.com/mmkal/expect-type) if the error messages are actually misleading.

The `toBe...` methods (like `toBeString`, `toBeNumber`, `toBeVoid` etc.) fail by resolving to a non-callable type when the `Actual` type under test doesn't match up. For example, the failure for an assertion like `expectTypeOf(1).toBeString()` will look something like this:

```
test/test.ts:999:999 - error TS2349: This expression is not callable.
Type 'ExpectString<number>' has no call signatures.

999 expectTypeOf(1).toBeString()
~~~~~~~~~~
```

The `This expression is not callable` part isn't all that helpful - the meaningful error is the next line, `Type 'ExpectString<number> has no call signatures`. This essentially means you passed a number but asserted it should be a string.

If TypeScript added support for ["throw" types](https://github.com/microsoft/TypeScript/pull/40468) these error messagess could be improved. Until then they will take a certain amount of squinting.

#### Concrete "expected" objects vs typeargs

Error messages for an assertion like this:

```ts
expectTypeOf({a: 1}).toEqualTypeOf({a: ''})
```

Will be less helpful than for an assertion like this:

```ts
expectTypeOf({a: 1}).toEqualTypeOf<{a: string}>()
```

This is because the TypeScript compiler needs to infer the typearg for the `.toEqualTypeOf({a: ''})` style, and this library can only mark it as a failure by comparing it against a generic `Mismatch` type. So, where possible, use a typearg rather than a concrete type for `.toEqualTypeOf` and `toMatchTypeOf`. If it's much more convenient to compare two concrete types, you can use `typeof`:

```ts
const one = valueFromFunctionOne({some: {complex: inputs}})
const two = valueFromFunctionTwo({some: {other: inputs}})

expectTypeOf(one).toEqualTypeof<typeof two>()
```

### Within test frameworks

#### Jest & `eslint-plugin-jest`
If you're using Jest along with `eslint-plugin-jest`, you will get warnings from the [`jest/expect-expect`](https://github.com/jest-community/eslint-plugin-jest/blob/master/docs/rules/expect-expect.md) rule, complaining that "Test has no assertions" for tests that only use `expectTypeOf()`.
If you're using Jest along with `eslint-plugin-jest`, you may get warnings from the [`jest/expect-expect`](https://github.com/jest-community/eslint-plugin-jest/blob/master/docs/rules/expect-expect.md) rule, complaining that "Test has no assertions" for tests that only use `expectTypeOf()`.

To remove this warning, configure the ESlint rule to consider `expectTypeOf` as an assertion:

Expand Down Expand Up @@ -495,7 +664,7 @@ The key differences in this project are:
- nullable types
- assertions on types "matching" rather than exact type equality, for "is-a" relationships e.g. `expectTypeOf(square).toMatchTypeOf<Shape>()`
- built into existing tooling. No extra build step, cli tool, IDE extension, or lint plugin is needed. Just import the function and start writing tests. Failures will be at compile time - they'll appear in your IDE and when you run `tsc`.
- small implementation with no dependencies. <200 lines of code - [take a look!](./src/index.ts) (tsd, for comparison, is [2.6MB](https://bundlephobia.com/result?p=tsd@0.13.1) because it ships a patched version of typescript).
- small implementation with no dependencies. [Take a look!](./src/index.ts) (tsd, for comparison, is [2.6MB](https://bundlephobia.com/result?p=tsd@0.13.1) because it ships a patched version of typescript).

## Contributing

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "expect-type",
"version": "0.16.0",
"version": "0.17.0-2",
"engines": {
"node": ">=12.0.0"
},
Expand Down Expand Up @@ -40,7 +40,7 @@
"eslint": "8.23.0",
"eslint-plugin-mmkal": "0.0.1-2",
"jest": "28.1.3",
"np": "8.0.1",
"np": "^8.0.4",
"strip-ansi": "6.0.1",
"ts-jest": "28.0.8",
"ts-morph": "16.0.0",
Expand Down