Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into deps
Browse files Browse the repository at this point in the history
  • Loading branch information
aryaemami59 committed Aug 13, 2024
2 parents cdc1585 + 7c63c23 commit 2ba4522
Show file tree
Hide file tree
Showing 8 changed files with 603 additions and 44 deletions.
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/result?p=tsd@0.13.1) 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

0 comments on commit 2ba4522

Please sign in to comment.