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

Rewrite Equal to use the equality check from ReadonlyEquivalent exclusively #21

Merged
merged 9 commits into from May 10, 2023
22 changes: 22 additions & 0 deletions README.md
Expand Up @@ -351,6 +351,12 @@ expectTypeOf([1, 2, 3]).items.toBeNumber()
expectTypeOf([1, 2, 3]).items.not.toBeString()
```

You can also compare arrays directly:

```typescript
expectTypeOf<any[]>().not.toEqualTypeOf<number[]>()
```

Check that functions never return:

```typescript
Expand Down Expand Up @@ -420,6 +426,22 @@ class C {

expectTypeOf<typeof A>().toEqualTypeOf<typeof C>()
```

Known limitation: Intersection types can cause issues with `toEqualTypeOf`:

```typescript
// @ts-expect-error the following line doesn't compile, even though the types are arguably the same.
// See https://github.com/mmkal/expect-type/pull/21
expectTypeOf<{a: 1} & {b: 2}>().toEqualTypeOf<{a: 1; b: 2}>()
```

To workaround, 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}>()
```
<!-- codegen:end -->

### Within test frameworks
Expand Down
114 changes: 71 additions & 43 deletions src/index.ts
Expand Up @@ -84,7 +84,17 @@ type ReadonlyEquivalent<X, Y> = Extends<
export type Extends<L, R> = IsNever<L> extends true ? IsNever<R> : [L] extends [R] ? true : false
export type StrictExtends<L, R> = Extends<DeepBrand<L>, DeepBrand<R>>

export type Equal<Left, Right> = And<[StrictExtends<Left, Right>, StrictExtends<Right, Left>]>
type StrictEqual<L, R> = (<T>() => T extends (L & T) | T ? true : false) extends <T>() => T extends (R & T) | T
? true
: false
? IsNever<L> extends IsNever<R>
? true
: false
: false

export type Equal<Left, Right, Branded = true> = Branded extends true
? And<[StrictExtends<Left, Right>, StrictExtends<Right, Left>]>
: StrictEqual<Left, Right>

export type Params<Actual> = Actual extends (...args: infer P) => any ? P : never
export type ConstructorParams<Actual> = Actual extends new (...args: infer P) => any
Expand All @@ -93,63 +103,78 @@ export type ConstructorParams<Actual> = Actual extends new (...args: infer P) =>
: P
: never

type MismatchArgs<B extends boolean, C extends boolean> = Eq<B, C> extends true ? [] : [never]

export interface ExpectTypeOf<Actual, B extends boolean> {
toBeAny: (...MISMATCH: MismatchArgs<IsAny<Actual>, B>) => true
toBeUnknown: (...MISMATCH: MismatchArgs<IsUnknown<Actual>, B>) => true
toBeNever: (...MISMATCH: MismatchArgs<IsNever<Actual>, B>) => true
toBeFunction: (...MISMATCH: MismatchArgs<Extends<Actual, (...args: any[]) => any>, B>) => true
toBeObject: (...MISMATCH: MismatchArgs<Extends<Actual, object>, B>) => true
toBeArray: (...MISMATCH: MismatchArgs<Extends<Actual, any[]>, B>) => true
toBeNumber: (...MISMATCH: MismatchArgs<Extends<Actual, number>, B>) => true
toBeString: (...MISMATCH: MismatchArgs<Extends<Actual, string>, B>) => true
toBeBoolean: (...MISMATCH: MismatchArgs<Extends<Actual, boolean>, B>) => true
toBeVoid: (...MISMATCH: MismatchArgs<Extends<Actual, void>, B>) => true
toBeSymbol: (...MISMATCH: MismatchArgs<Extends<Actual, symbol>, B>) => true
toBeNull: (...MISMATCH: MismatchArgs<Extends<Actual, null>, B>) => true
toBeUndefined: (...MISMATCH: MismatchArgs<Extends<Actual, undefined>, B>) => true
toBeNullable: (...MISMATCH: MismatchArgs<Not<Equal<Actual, NonNullable<Actual>>>, B>) => true
type MismatchArgs<ActualResult extends boolean, ExpectedResult extends boolean> = Eq<
ActualResult,
ExpectedResult
> extends true
? []
: [never]

export interface ExpectTypeOfOptions {
positive: boolean
branded: boolean
}
export interface ExpectTypeOf<Actual, Options extends ExpectTypeOfOptions> {
toBeAny: (...MISMATCH: MismatchArgs<IsAny<Actual>, Options['positive']>) => true
toBeUnknown: (...MISMATCH: MismatchArgs<IsUnknown<Actual>, Options['positive']>) => true
toBeNever: (...MISMATCH: MismatchArgs<IsNever<Actual>, Options['positive']>) => true
toBeFunction: (...MISMATCH: MismatchArgs<Extends<Actual, (...args: any[]) => any>, Options['positive']>) => true
toBeObject: (...MISMATCH: MismatchArgs<Extends<Actual, object>, Options['positive']>) => true
toBeArray: (...MISMATCH: MismatchArgs<Extends<Actual, any[]>, Options['positive']>) => true
toBeNumber: (...MISMATCH: MismatchArgs<Extends<Actual, number>, Options['positive']>) => true
toBeString: (...MISMATCH: MismatchArgs<Extends<Actual, string>, Options['positive']>) => true
toBeBoolean: (...MISMATCH: MismatchArgs<Extends<Actual, boolean>, Options['positive']>) => true
toBeVoid: (...MISMATCH: MismatchArgs<Extends<Actual, void>, Options['positive']>) => true
toBeSymbol: (...MISMATCH: MismatchArgs<Extends<Actual, symbol>, Options['positive']>) => true
toBeNull: (...MISMATCH: MismatchArgs<Extends<Actual, null>, Options['positive']>) => true
toBeUndefined: (...MISMATCH: MismatchArgs<Extends<Actual, undefined>, Options['positive']>) => true
toBeNullable: (
...MISMATCH: MismatchArgs<Not<Equal<Actual, NonNullable<Actual>, Options['branded']>>, Options['positive']>
) => true
toMatchTypeOf: {
<Expected>(...MISMATCH: MismatchArgs<Extends<Actual, Expected>, B>): true
<Expected>(expected: Expected, ...MISMATCH: MismatchArgs<Extends<Actual, Expected>, B>): true
<Expected>(...MISMATCH: MismatchArgs<Extends<Actual, Expected>, Options['positive']>): true
<Expected>(expected: Expected, ...MISMATCH: MismatchArgs<Extends<Actual, Expected>, Options['positive']>): true
}
toEqualTypeOf: {
<Expected>(...MISMATCH: MismatchArgs<Equal<Actual, Expected>, B>): true
<Expected>(expected: Expected, ...MISMATCH: MismatchArgs<Equal<Actual, Expected>, B>): true
<Expected>(...MISMATCH: MismatchArgs<Equal<Actual, Expected, Options['branded']>, Options['positive']>): true
<Expected>(
expected: Expected,
...MISMATCH: MismatchArgs<Equal<Actual, Expected, Options['branded']>, Options['positive']>
): true
}
toBeCallableWith: B extends true ? (...args: Params<Actual>) => true : never
toBeConstructibleWith: B extends true ? (...args: ConstructorParams<Actual>) => true : never
toBeCallableWith: Options['positive'] extends true ? (...args: Params<Actual>) => true : never
toBeConstructibleWith: Options['positive'] extends true ? (...args: ConstructorParams<Actual>) => true : never
toHaveProperty: <K extends string>(
key: K,
...MISMATCH: MismatchArgs<Extends<K, keyof Actual>, B>
) => K extends keyof Actual ? ExpectTypeOf<Actual[K], B> : true
extract: <V>(v?: V) => ExpectTypeOf<Extract<Actual, V>, B>
exclude: <V>(v?: V) => ExpectTypeOf<Exclude<Actual, V>, B>
parameter: <K extends keyof Params<Actual>>(number: K) => ExpectTypeOf<Params<Actual>[K], B>
parameters: ExpectTypeOf<Params<Actual>, B>
constructorParameters: ExpectTypeOf<ConstructorParams<Actual>, B>
thisParameter: ExpectTypeOf<ThisParameterType<Actual>, B>
instance: Actual extends new (...args: any[]) => infer I ? ExpectTypeOf<I, B> : never
returns: Actual extends (...args: any[]) => infer R ? ExpectTypeOf<R, B> : never
resolves: Actual extends PromiseLike<infer R> ? ExpectTypeOf<R, B> : never
items: Actual extends ArrayLike<infer R> ? ExpectTypeOf<R, B> : never
guards: Actual extends (v: any, ...args: any[]) => v is infer T ? ExpectTypeOf<T, B> : never
...MISMATCH: MismatchArgs<Extends<K, keyof Actual>, Options['positive']>
) => K extends keyof Actual ? ExpectTypeOf<Actual[K], Options> : true
extract: <V>(v?: V) => ExpectTypeOf<Extract<Actual, V>, Options>
exclude: <V>(v?: V) => ExpectTypeOf<Exclude<Actual, V>, Options>
parameter: <K extends keyof Params<Actual>>(number: K) => ExpectTypeOf<Params<Actual>[K], Options>
parameters: ExpectTypeOf<Params<Actual>, Options>
constructorParameters: ExpectTypeOf<ConstructorParams<Actual>, Options>
thisParameter: ExpectTypeOf<ThisParameterType<Actual>, Options>
instance: Actual extends new (...args: any[]) => infer I ? ExpectTypeOf<I, Options> : never
returns: Actual extends (...args: any[]) => infer R ? ExpectTypeOf<R, Options> : never
resolves: Actual extends PromiseLike<infer R> ? ExpectTypeOf<R, Options> : never
items: Actual extends ArrayLike<infer R> ? ExpectTypeOf<R, Options> : never
guards: Actual extends (v: any, ...args: any[]) => v is infer T ? ExpectTypeOf<T, Options> : never
asserts: Actual extends (v: any, ...args: any[]) => asserts v is infer T
? // Guard methods `(v: any) => asserts v is T` does not actually defines a return type. Thus, any function taking 1 argument matches the signature before.
// In case the inferred assertion type `R` could not be determined (so, `unknown`), consider the function as a non-guard, and return a `never` type.
// See https://github.com/microsoft/TypeScript/issues/34636
unknown extends T
? never
: ExpectTypeOf<T, B>
: ExpectTypeOf<T, Options>
: never
not: Omit<ExpectTypeOf<Actual, Not<B>>, 'not'>
branded: Omit<ExpectTypeOf<Actual, {positive: Options['positive']; branded: true}>, 'branded'>
not: Omit<ExpectTypeOf<Actual, {positive: Not<Options['positive']>; branded: Options['branded']}>, 'not'>
}
const fn: any = () => true

export type _ExpectTypeOf = {
<Actual>(actual: Actual): ExpectTypeOf<Actual, true>
<Actual>(): ExpectTypeOf<Actual, true>
<Actual>(actual: Actual): ExpectTypeOf<Actual, {positive: true; branded: false}>
<Actual>(): ExpectTypeOf<Actual, {positive: true; branded: false}>
}

/**
Expand All @@ -174,7 +199,9 @@ export type _ExpectTypeOf = {
* @description
* See the [full docs](https://npmjs.com/package/expect-type#documentation) for lots more examples.
*/
export const expectTypeOf: _ExpectTypeOf = <Actual>(_actual?: Actual): ExpectTypeOf<Actual, true> => {
export const expectTypeOf: _ExpectTypeOf = <Actual>(
_actual?: Actual,
): ExpectTypeOf<Actual, {positive: true; branded: false}> => {
const nonFunctionProperties = [
'parameters',
'returns',
Expand All @@ -186,6 +213,7 @@ export const expectTypeOf: _ExpectTypeOf = <Actual>(_actual?: Actual): ExpectTyp
'instance',
'guards',
'asserts',
'branded',
] as const
type Keys = keyof ExpectTypeOf<any, any>

Expand Down Expand Up @@ -220,5 +248,5 @@ export const expectTypeOf: _ExpectTypeOf = <Actual>(_actual?: Actual): ExpectTyp
const getterProperties: readonly Keys[] = nonFunctionProperties
getterProperties.forEach((prop: Keys) => Object.defineProperty(obj, prop, {get: () => expectTypeOf({})}))

return obj as ExpectTypeOf<Actual, true>
return obj as ExpectTypeOf<Actual, {positive: true; branded: false}>
}