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 overloads support, attempt 2 #83

Merged
merged 46 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fbeb8b9
better overloads support
mmkal Jul 26, 2024
7270eea
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 26, 2024
0aa011e
overloads.ts
mmkal Aug 1, 2024
29392af
reduce vertical sprawl
mmkal Aug 1, 2024
d3f37f8
jsdoc for overloads.ts
mmkal Aug 1, 2024
f03401d
create a utils.ts file to avoid circular reference
mmkal Aug 1, 2024
70df924
rm some full stops
mmkal Aug 1, 2024
6cb3917
rm limitation that only applies to ts<5.3
mmkal Aug 1, 2024
6cdd447
Let `.toBeCallableWith` narrow down the return type
mmkal Aug 1, 2024
725db63
Merge remote-tracking branch 'origin/main' into overloadsagain
mmkal Aug 1, 2024
8dd3f3c
Update README.md
mmkal Aug 1, 2024
0c0a3f1
Update README.md
mmkal Aug 1, 2024
e48f831
improve overloads limitation explainer
mmkal Aug 1, 2024
4fcc32f
erge branch 'overloadsagain' of https://github.com/mmkal/expect-type …
mmkal Aug 1, 2024
a4b0776
Fix issue codeblock related issue in `README.md`
aryaemami59 Aug 2, 2024
a0e80fc
Fix whitespace issues in `utils.ts` affecting JSDoc placement
aryaemami59 Aug 2, 2024
4128880
Remove `ts52`
aryaemami59 Aug 2, 2024
0eb9beb
Merge remote-tracking branch 'origin/main' into overloadsagain
mmkal Aug 5, 2024
f9c119f
Merge branch 'overloadsagain' of https://github.com/mmkal/expect-type…
mmkal Aug 5, 2024
3e9465c
Merge branch 'main' of https://github.com/mmkal/expect-type into over…
aryaemami59 Aug 6, 2024
e12d7df
Merge branch 'overloadsagain' of https://github.com/mmkal/expect-type…
aryaemami59 Aug 6, 2024
f74d538
Merge branch 'main' of https://github.com/mmkal/expect-type into over…
aryaemami59 Aug 6, 2024
28e5e31
Change typescript to TypeScript
aryaemami59 Aug 6, 2024
4633d8d
.parameter and .parameaters, edge cases, docs
mmkal Aug 6, 2024
0845909
runtime .parameter/.parameters + avoid weird tuple problem
mmkal Aug 6, 2024
d96914b
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 6, 2024
878131e
Merge remote-tracking branch 'origin/main' into overloadsagain
mmkal Aug 6, 2024
a557a70
Merge branch 'main' into overloadsagain
mmkal Aug 7, 2024
1a515c5
refactor to use union rather than tuple
mmkal Aug 7, 2024
e145315
make it work for ts5.2
mmkal Aug 7, 2024
a6b6ea1
update readme
mmkal Aug 7, 2024
367890a
simplify pre-5.3 helper, we can use never now
mmkal Aug 7, 2024
cd67a0f
OverloadsNarrowedByParameters helpers
mmkal Aug 7, 2024
f471e28
slightly nicer docs
mmkal Aug 8, 2024
a2d6436
constructor parameters
mmkal Aug 8, 2024
16065ad
add deepbrand todok
mmkal Aug 8, 2024
d5dedb9
Update types.test.ts
mmkal Aug 8, 2024
0a79096
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2024
0bf9f3c
update snapshots
mmkal Aug 8, 2024
8e5c20f
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2024
d19aa7e
DeepBrand todos
mmkal Aug 9, 2024
1eb74cd
Merge branch 'main' into overloadsagain
mmkal Aug 11, 2024
55bd6d8
rm ConstructorParams
mmkal Aug 11, 2024
c98bfd6
suggestions
mmkal Aug 11, 2024
4c6b2e3
update .constructorParameters test
mmkal Aug 11, 2024
1cd637d
link issue in readme too
mmkal Aug 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 140 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ See below for lots more examples.
- [Internal type helpers](#internal-type-helpers)
- [Error messages](#error-messages)
- [Concrete "expected" objects vs type arguments](#concrete-expected-objects-vs-type-arguments)
- [Overloaded functions](#overloaded-functions)
- [Within test frameworks](#within-test-frameworks)
- [Vitest](#vitest)
- [Jest & `eslint-plugin-jest`](#jest--eslint-plugin-jest)
- [Limitations](#limitations)
- [Similar projects](#similar-projects)
- [Comparison](#comparison)
- [TypeScript backwards-compatibility](#typescript-backwards-compatibility)
- [Contributing](#contributing)
- [Documentation of limitations through tests](#documentation-of-limitations-through-tests)
<!-- codegen:end -->

## Installation and usage
Expand Down Expand Up @@ -316,6 +319,35 @@ expectTypeOf<HasParam>().parameters.toEqualTypeOf<[string]>()
expectTypeOf<HasParam>().returns.toBeVoid()
```

Up to ten overloads will produce union types for `.parameters` and `.returns`:

```typescript
type Factorize = {
(input: number): number[]
(input: bigint): bigint[]
}

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[number] | [bigint]>()
expectTypeOf<Factorize>().returns.toEqualTypeOf<number[] | bigint[]>()

expectTypeOf<Factorize>().parameter(0).toEqualTypeOf<number | bigint>()
```

Note that these aren't exactly like TypeScript's built-in Parameters<...> and ReturnType<...>:

The TypeScript builtins simply choose a single overload (see the [Overloaded functions](#overloaded-functions) section for more information)

```typescript
type Factorize = {
(input: number): number[]
(input: bigint): bigint[]
}

// overload using `number` is ignored!
expectTypeOf<Parameters<Factorize>>().toEqualTypeOf<[bigint]>()
expectTypeOf<ReturnType<Factorize>>().toEqualTypeOf<bigint[]>()
```

More examples of ways to work with functions - parameters using `.parameter(n)` or `.parameters`, and return values using `.returns`:

```typescript
Expand All @@ -337,6 +369,56 @@ const twoArgFunc = (a: number, b: string) => ({a, b})
expectTypeOf(twoArgFunc).parameters.toEqualTypeOf<[number, string]>()
```

`.toBeCallableWith` allows for overloads. You can also use it to narrow down the return type for given input parameters.:

```typescript
type Factorize = {
(input: number): number[]
(input: bigint): bigint[]
}

expectTypeOf<Factorize>().toBeCallableWith(6)
expectTypeOf<Factorize>().toBeCallableWith(6n)
```

`.toBeCallableWith` returns a type that can be used to narrow down the return type for given input parameters.:

```typescript
type Factorize = {
(input: number): number[]
(input: bigint): bigint[]
}
expectTypeOf<Factorize>().toBeCallableWith(6).returns.toEqualTypeOf<number[]>()
expectTypeOf<Factorize>().toBeCallableWith(6n).returns.toEqualTypeOf<bigint[]>()
```

`.toBeCallableWith` can be used to narrow down the parameters of a function:

```typescript
type Delete = {
(path: string): void
(paths: string[], options?: {force: boolean}): void
}

expectTypeOf<Delete>().toBeCallableWith('abc').parameters.toEqualTypeOf<[string]>()
expectTypeOf<Delete>()
.toBeCallableWith(['abc', 'def'], {force: true})
.parameters.toEqualTypeOf<[string[], {force: boolean}?]>()

expectTypeOf<Delete>().toBeCallableWith('abc').parameter(0).toBeString()
expectTypeOf<Delete>().toBeCallableWith('abc').parameter(1).toBeUndefined()

expectTypeOf<Delete>()
.toBeCallableWith(['abc', 'def', 'ghi'])
.parameter(0)
.toEqualTypeOf<string[]>()

expectTypeOf<Delete>()
.toBeCallableWith(['abc', 'def', 'ghi'])
.parameter(1)
.toEqualTypeOf<{force: boolean} | undefined>()
```

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

```typescript
Expand Down Expand Up @@ -369,7 +451,37 @@ expectTypeOf(Date).toBeConstructibleWith(0)
expectTypeOf(Date).toBeConstructibleWith(new Date())
expectTypeOf(Date).toBeConstructibleWith()

expectTypeOf(Date).constructorParameters.toEqualTypeOf<[] | [string | number | Date]>()
expectTypeOf(Date).constructorParameters.toEqualTypeOf<
| []
| [value: string | number]
| [value: string | number | Date]
| [
year: number,
monthIndex: number,
date?: number | undefined,
hours?: number | undefined,
minutes?: number | undefined,
seconds?: number | undefined,
ms?: number | undefined,
]
>()
```

Constructor overloads:

```typescript
class DBConnection {
constructor()
constructor(connectionString: string)
constructor(options: {host: string; port: number})
constructor(..._: unknown[]) {}
}

expectTypeOf(DBConnection).toBeConstructibleWith()
expectTypeOf(DBConnection).toBeConstructibleWith('localhost')
expectTypeOf(DBConnection).toBeConstructibleWith({host: 'localhost', port: 1234})
// @ts-expect-error - as when calling `new DBConnection(...)` you can't actually use the `(...args: unknown[])` overlaod, it's purely for the implementation.
expectTypeOf(DBConnection).toBeConstructibleWith(1, 2)
```

Check function `this` parameters:
Expand Down Expand Up @@ -561,6 +673,21 @@ expectTypeOf(B).instance.toEqualTypeOf<{b: string; foo: () => void}>()
```
<!-- codegen:end -->

Overloads limitation for TypeScript <5.3: Due to a [TypeScript bug fixed in 5.3](https://github.com/microsoft/TypeScript/issues/28867), overloaded functions which include an overload resembling `(...args: unknown[]) => unknown` will exclude `unknown[]` from `.parameters` and exclude `unknown` from `.returns`:

```typescript
type Factorize = {
(...args: unknown[]): unknown
(input: number): number[]
(input: bigint): bigint[]
}

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[number] | [bigint]>()
expectTypeOf<Factorize>().returns.toEqualTypeOf<number[] | bigint[]>()
```

This overload, however, allows any input and returns an unknown output anyway, so it's not very useful. If you are worried about this for some reason, you'll have to update TypeScript to 5.3+.

### Why is my assertion failing?

For complex types, an assertion might fail when it should if the `Actual` type contains a deeply-nested intersection type but the `Expected` doesn't. In these cases you can use `.branded` as described above:
Expand Down Expand Up @@ -641,6 +768,10 @@ const two = valueFromFunctionTwo({some: {other: inputs}})
expectTypeOf(one).toEqualTypeof<typeof two>()
```

### Overloaded functions

Due to a TypeScript [design limitation](https://github.com/microsoft/TypeScript/issues/32164#issuecomment-506810756), the native TypeScript `Parameters<...>` and `ReturnType<...>` helpers only return types from one variant of an overloaded function. This limitation doesn't apply to expect-type, since it is not used to author TypeScript code, only to assert on existing types. So, we use a workaround for this TypeScript behaviour to assert on _all_ overloads as a union (actually, not necessarily _all_ - we cap out at 10 overloads).

### Within test frameworks

### Vitest
Expand Down Expand Up @@ -720,6 +851,10 @@ The key differences in this project are:
- 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. [Take a look!](./src/index.ts) (tsd, for comparison, is [2.6MB](https://bundlephobia.com/[email protected]) because it ships a patched version of TypeScript).

## TypeScript backwards-compatibility

There is a CI job called `test-types` that checks whether the tests still pass with certain older TypeScript versions. To check the supported TypeScript versions, [refer to the job definition](./.github/workflows/ci.yml).

## Contributing

In most cases, it's worth checking existing issues or creating one to discuss a new feature or a bug fix before opening a pull request.
Expand All @@ -729,3 +864,7 @@ Once you're ready to make a pull request: clone the repo, and install pnpm if yo
If you're adding a feature, you should write a self-contained usage example in the form of a test, in [test/usage.test.ts](./test/usage.test.ts). This file is used to populate the bulk of this readme using [eslint-plugin-codegen](https://npmjs.com/package/eslint-plugin-codegen), and to generate an ["errors" test file](./test/errors.test.ts), which captures the error messages that are emitted for failing assertions by the TypeScript compiler. So, the test name should be written as a human-readable sentence explaining the usage example. Have a look at the existing tests for an idea of the style.

After adding the tests, run `npm run lint -- --fix` to update the readme, and `npm test -- --updateSnapshot` to update the errors test. The generated documentation and tests should be pushed to the same branch as the source code, and submitted as a pull request. CI will test that the docs and tests are up to date if you forget to run these commands.

### Documentation of limitations through tests

Limitations of the library are documented through tests in `usage.test.ts`. This means that if a future TypeScript version (or library version) fixes the limitation, the test will start failing, and it will be automatically removed from the documentation once it no longer applies.
30 changes: 20 additions & 10 deletions src/branding.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ConstructorOverloadParameters, NumOverloads, OverloadsInfoUnion} from './overloads'
import {
IsNever,
IsAny,
Expand All @@ -6,7 +7,7 @@ import {
RequiredKeys,
OptionalKeys,
MutuallyExtends,
ConstructorParams,
UnionToTuple,
} from './utils'

/**
Expand Down Expand Up @@ -40,17 +41,26 @@ export type DeepBrand<T> =
: T extends new (...args: any[]) => any
? {
type: 'constructor'
params: ConstructorParams<T>
params: ConstructorOverloadParameters<T>
instance: DeepBrand<InstanceType<Extract<T, new (...args: any) => any>>>
}
: T extends (...args: infer P) => infer R // avoid functions with different params/return values matching
? {
type: 'function'
params: DeepBrand<P>
return: DeepBrand<R>
this: DeepBrand<ThisParameterType<T>>
props: DeepBrand<Omit<T, keyof Function>>
}
? NumOverloads<T> extends 1
? {
type: 'function'
params: DeepBrand<P>
return: DeepBrand<R>
this: DeepBrand<ThisParameterType<T>>
props: DeepBrand<Omit<T, keyof Function>>
}
: UnionToTuple<OverloadsInfoUnion<T>> extends infer OverloadsTuple
? {
type: 'overloads'
overloads: {
[K in keyof OverloadsTuple]: DeepBrand<OverloadsTuple[K]>
}
}
: never
: T extends any[]
? {
type: 'array'
Expand All @@ -66,7 +76,7 @@ export type DeepBrand<T> =
readonly: ReadonlyKeys<T>
required: RequiredKeys<T>
optional: OptionalKeys<T>
constructorParams: DeepBrand<ConstructorParams<T>>
constructorParams: DeepBrand<ConstructorOverloadParameters<T>>
}

/**
Expand Down
37 changes: 22 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ import {
ExpectNullable,
} from './messages'
import {
StrictEqualUsingTSInternalIdenticalToOperator,
AValue,
MismatchArgs,
Extends,
Params,
ConstructorParams,
} from './utils'
ConstructorOverloadParameters,
OverloadParameters,
OverloadReturnTypes,
OverloadsNarrowedByParameters,
} from './overloads'
import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends} from './utils'

export * from './branding' // backcompat, consider removing in next major version
export * from './utils' // backcompat, consider removing in next major version
Expand Down Expand Up @@ -502,7 +501,7 @@ export interface BaseExpectTypeOf<Actual, Options extends {positive: boolean}> {
* Checks whether a function is callable with the given parameters.
*
* __Note__: You cannot negate this assertion with
* {@linkcode PositiveExpectTypeOf.not `.not`} you need to use
* {@linkcode PositiveExpectTypeOf.not `.not`}, you need to use
* `ts-expect-error` instead.
*
* @example
Expand All @@ -519,7 +518,11 @@ export interface BaseExpectTypeOf<Actual, Options extends {positive: boolean}> {
* @param args - The arguments to check for callability.
* @returns `true`.
*/
toBeCallableWith: Options['positive'] extends true ? (...args: Params<Actual>) => true : never
toBeCallableWith: Options['positive'] extends true
? <A extends OverloadParameters<Actual>>(
...args: A
) => ExpectTypeOf<OverloadsNarrowedByParameters<Actual, A>, Options>
: never

/**
* Checks whether a class is constructible with the given parameters.
Expand All @@ -538,7 +541,9 @@ export interface BaseExpectTypeOf<Actual, Options extends {positive: boolean}> {
* @param args - The arguments to check for constructibility.
* @returns `true`.
*/
toBeConstructibleWith: Options['positive'] extends true ? (...args: ConstructorParams<Actual>) => true : never
toBeConstructibleWith: Options['positive'] extends true
? <A extends ConstructorOverloadParameters<Actual>>(...args: A) => true
: never

/**
* Equivalent to the {@linkcode Extract} utility type.
Expand Down Expand Up @@ -666,7 +671,7 @@ export interface BaseExpectTypeOf<Actual, Options extends {positive: boolean}> {
* @param index - The index of the parameter to extract.
* @returns The extracted parameter type.
*/
parameter: <Index extends keyof Params<Actual>>(index: Index) => ExpectTypeOf<Params<Actual>[Index], Options>
parameter: <Index extends number>(index: Index) => ExpectTypeOf<OverloadParameters<Actual>[Index], Options>

/**
* Equivalent to the {@linkcode Parameters} utility type.
Expand All @@ -684,21 +689,23 @@ export interface BaseExpectTypeOf<Actual, Options extends {positive: boolean}> {
* expectTypeOf(hasParam).parameters.toEqualTypeOf<[string]>()
* ```
*/
parameters: ExpectTypeOf<Params<Actual>, Options>
parameters: ExpectTypeOf<OverloadParameters<Actual>, Options>

/**
* Equivalent to the {@linkcode ConstructorParameters} utility type.
* Extracts constructor parameters as an array of values and
* perform assertions on them with this method.
*
* For overloaded constructors it will return a union of all possible parameter-tuples.
*
* @example
* ```ts
* expectTypeOf(Date).constructorParameters.toEqualTypeOf<
* [] | [string | number | Date]
* >()
* ```
*/
constructorParameters: ExpectTypeOf<ConstructorParams<Actual>, Options>
constructorParameters: ExpectTypeOf<ConstructorOverloadParameters<Actual>, Options>

/**
* Equivalent to the {@linkcode ThisParameterType} utility type.
Expand Down Expand Up @@ -738,7 +745,7 @@ export interface BaseExpectTypeOf<Actual, Options extends {positive: boolean}> {
* expectTypeOf((a: number) => [a, a]).returns.toEqualTypeOf([1, 2])
* ```
*/
returns: Actual extends (...args: any[]) => infer R ? ExpectTypeOf<R, Options> : never
returns: Actual extends Function ? ExpectTypeOf<OverloadReturnTypes<Actual>, Options> : never

/**
* Extracts resolved value of a Promise,
Expand Down Expand Up @@ -900,9 +907,9 @@ export const expectTypeOf: _ExpectTypeOf = <Actual>(
toBeNullable: fn,
toMatchTypeOf: fn,
toEqualTypeOf: fn,
toBeCallableWith: fn,
toBeConstructibleWith: fn,
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
toBeCallableWith: expectTypeOf,
extract: expectTypeOf,
exclude: expectTypeOf,
pick: expectTypeOf,
Expand Down
Loading