expect-type
Advanced tools
Comparing version 0.16.0 to 0.17.0-1
@@ -12,2 +12,16 @@ export declare type Not<T extends boolean> = T extends true ? false : true; | ||
export declare type IsNeverOrAny<T> = Or<[IsNever<T>, IsAny<T>]>; | ||
export declare type BrandSpecial<T> = IsAny<T> extends true ? { | ||
special: true; | ||
type: 'any'; | ||
} : IsUnknown<T> extends true ? { | ||
special: true; | ||
type: 'unknown'; | ||
} : IsNever<T> extends true ? { | ||
special: true; | ||
type: 'never'; | ||
} : never; | ||
export declare type PrintType<T> = IsUnknown<T> extends true ? 'unknown' : IsNever<T> extends true ? 'never' : IsAny<T> extends true ? never : boolean extends T ? 'boolean' : T extends boolean ? `literal boolean: ${T}` : T extends string ? string extends T ? 'string' : `literal string: ${T}` : T extends number ? number extends T ? 'number' : `literal number: ${T}` : T extends null ? 'null' : T extends undefined ? 'undefined' : T extends (...args: any[]) => any ? 'function' : '...'; | ||
export declare type MismatchInfo<Actual, Expected> = And<[Extends<PrintType<Actual>, '...'>, Not<IsAny<Actual>>]> extends true ? { | ||
[K in keyof Actual | keyof Expected]: MismatchInfo<K extends keyof Actual ? Actual[K] : never, K extends keyof Expected ? Expected[K] : never>; | ||
} : StrictEqualUsingBranding<Actual, Expected> extends true ? Actual : `Expected: ${PrintType<Expected>}, Actual: ${PrintType<Exclude<Actual, Expected>>}`; | ||
/** | ||
@@ -39,2 +53,3 @@ * Recursively walk a type and replace it with a branded type related to the original. This is useful for | ||
this: DeepBrand<ThisParameterType<T>>; | ||
props: DeepBrand<Omit<T, keyof Function>>; | ||
} : T extends any[] ? { | ||
@@ -68,8 +83,17 @@ type: 'array'; | ||
export declare type Extends<L, R> = IsNever<L> extends true ? IsNever<R> : [L] extends [R] ? true : false; | ||
export declare type StrictExtends<L, R> = Extends<DeepBrand<L>, DeepBrand<R>>; | ||
declare 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 declare type Equal<Left, Right, Branded = true> = Branded extends true ? And<[StrictExtends<Left, Right>, StrictExtends<Right, Left>]> : StrictEqual<Left, Right>; | ||
export declare type ExtendsUsingBranding<L, R> = Extends<DeepBrand<L>, DeepBrand<R>>; | ||
declare type StrictEqualUsingTSInternalIdenticalToOperator<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 declare type StrictEqualUsingBranding<Left, Right> = And<[ExtendsUsingBranding<Left, Right>, ExtendsUsingBranding<Right, Left>]>; | ||
export declare type HopefullyPerformantEqual<Left, Right> = StrictEqualUsingTSInternalIdenticalToOperator<Left, Right> extends true ? true : StrictEqualUsingBranding<Left, Right>; | ||
export declare type Params<Actual> = Actual extends (...args: infer P) => any ? P : never; | ||
export declare type ConstructorParams<Actual> = Actual extends new (...args: infer P) => any ? Actual extends new () => any ? P | [] : P : never; | ||
declare type MismatchArgs<ActualResult extends boolean, ExpectedResult extends boolean> = Eq<ActualResult, ExpectedResult> extends true ? [] : [never]; | ||
declare const error: unique symbol; | ||
declare type Mismatch = { | ||
[error]: 'mismatch'; | ||
}; | ||
/** A type which should match anything passed as a value but *doesn't* match `Mismatch` - helps TypeScript select the right overload for `toEqualTypeOf` and `toMatchTypeOf`. */ | ||
declare type AValue = { | ||
[error]?: undefined; | ||
} | string | number | boolean | symbol | bigint | null | undefined | void; | ||
declare type MismatchArgs<ActualResult extends boolean, ExpectedResult extends boolean> = Eq<ActualResult, ExpectedResult> extends true ? [] : [Mismatch]; | ||
export interface ExpectTypeOfOptions { | ||
@@ -79,28 +103,125 @@ positive: 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; | ||
declare type Inverted<T> = { | ||
[error]: T; | ||
}; | ||
declare type ExpectNull<T> = { | ||
[error]: T; | ||
result: StrictEqualUsingTSInternalIdenticalToOperator<T, null>; | ||
}; | ||
declare type ExpectUndefined<T> = { | ||
[error]: T; | ||
result: StrictEqualUsingTSInternalIdenticalToOperator<T, undefined>; | ||
}; | ||
declare type ExpectNumber<T> = { | ||
[error]: T; | ||
result: StrictEqualUsingTSInternalIdenticalToOperator<T, number>; | ||
}; | ||
declare type ExpectString<T> = { | ||
[error]: T; | ||
result: StrictEqualUsingTSInternalIdenticalToOperator<T, string>; | ||
}; | ||
declare type ExpectBoolean<T> = { | ||
[error]: T; | ||
result: StrictEqualUsingTSInternalIdenticalToOperator<T, boolean>; | ||
}; | ||
declare type ExpectVoid<T> = { | ||
[error]: T; | ||
result: StrictEqualUsingTSInternalIdenticalToOperator<T, void>; | ||
}; | ||
declare type ExpectFunction<T> = { | ||
[error]: T; | ||
result: Extends<T, (...args: any[]) => any>; | ||
}; | ||
declare type ExpectObject<T> = { | ||
[error]: T; | ||
result: Extends<T, object>; | ||
}; | ||
declare type ExpectArray<T> = { | ||
[error]: T; | ||
result: Extends<T, any[]>; | ||
}; | ||
declare type ExpectSymbol<T> = { | ||
[error]: T; | ||
result: Extends<T, symbol>; | ||
}; | ||
declare type ExpectAny<T> = { | ||
[error]: T; | ||
result: IsAny<T>; | ||
}; | ||
declare type ExpectUnknown<T> = { | ||
[error]: T; | ||
result: IsUnknown<T>; | ||
}; | ||
declare type ExpectNever<T> = { | ||
[error]: T; | ||
result: IsNever<T>; | ||
}; | ||
declare type ExpectNullable<T> = { | ||
[error]: T; | ||
result: Not<StrictEqualUsingBranding<T, NonNullable<T>>>; | ||
}; | ||
declare type Scolder<Expecter extends { | ||
result: boolean; | ||
}, Options extends { | ||
positive: boolean; | ||
}> = Expecter['result'] extends Options['positive'] ? () => true : Options['positive'] extends true ? Expecter : Inverted<Expecter>; | ||
export interface PositiveExpectTypeOf<Actual> extends BaseExpectTypeOf<Actual, { | ||
positive: true; | ||
branded: false; | ||
}> { | ||
toEqualTypeOf: { | ||
<Expected extends StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected> extends true ? unknown : MismatchInfo<Actual, Expected>>(value: Expected & AValue, // reason for `& AValue`: make sure this is only the selected overload when the end-user passes a value for an inferred typearg. The `Mismatch` type does match `AValue`. | ||
...MISMATCH: MismatchArgs<StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected>, true>): true; | ||
<Expected extends StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected> extends true ? unknown : MismatchInfo<Actual, Expected>>(...MISMATCH: MismatchArgs<StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected>, true>): true; | ||
}; | ||
toMatchTypeOf: { | ||
<Expected>(...MISMATCH: MismatchArgs<Extends<Actual, Expected>, Options['positive']>): true; | ||
<Expected>(expected: Expected, ...MISMATCH: MismatchArgs<Extends<Actual, Expected>, Options['positive']>): true; | ||
<Expected extends Extends<Actual, Expected> extends true ? unknown : MismatchInfo<Actual, Expected>>(value: Expected & AValue, // reason for `& AValue`: make sure this is only the selected overload when the end-user passes a value for an inferred typearg. The `Mismatch` type does match `AValue`. | ||
...MISMATCH: MismatchArgs<Extends<Actual, Expected>, true>): true; | ||
<Expected extends Extends<Actual, Expected> extends true ? unknown : MismatchInfo<Actual, Expected>>(...MISMATCH: MismatchArgs<Extends<Actual, Expected>, true>): true; | ||
}; | ||
toHaveProperty: <K extends keyof Actual>(key: K, ...MISMATCH: MismatchArgs<Extends<K, keyof Actual>, true>) => K extends keyof Actual ? PositiveExpectTypeOf<Actual[K]> : true; | ||
not: NegativeExpectTypeOf<Actual>; | ||
branded: { | ||
toEqualTypeOf: <Expected extends StrictEqualUsingBranding<Actual, Expected> extends true ? unknown : MismatchInfo<Actual, Expected>>(...MISMATCH: MismatchArgs<StrictEqualUsingBranding<Actual, Expected>, true>) => true; | ||
}; | ||
} | ||
export interface NegativeExpectTypeOf<Actual> extends BaseExpectTypeOf<Actual, { | ||
positive: false; | ||
}> { | ||
toEqualTypeOf: { | ||
<Expected>(...MISMATCH: MismatchArgs<Equal<Actual, Expected, Options['branded']>, Options['positive']>): true; | ||
<Expected>(expected: Expected, ...MISMATCH: MismatchArgs<Equal<Actual, Expected, Options['branded']>, Options['positive']>): true; | ||
<Expected>(value: Expected & AValue, ...MISMATCH: MismatchArgs<StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected>, false>): true; | ||
<Expected>(...MISMATCH: MismatchArgs<StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected>, false>): true; | ||
}; | ||
toMatchTypeOf: { | ||
<Expected>(value: Expected & AValue, // reason for `& AValue`: make sure this is only the selected overload when the end-user passes a value for an inferred typearg. The `Mismatch` type does match `AValue`. | ||
...MISMATCH: MismatchArgs<Extends<Actual, Expected>, false>): true; | ||
<Expected>(...MISMATCH: MismatchArgs<Extends<Actual, Expected>, false>): true; | ||
}; | ||
toHaveProperty: <K extends string | number | symbol>(key: K, ...MISMATCH: MismatchArgs<Extends<K, keyof Actual>, false>) => true; | ||
branded: { | ||
toEqualTypeOf: <Expected>(...MISMATCH: MismatchArgs<StrictEqualUsingTSInternalIdenticalToOperator<Actual, Expected>, false>) => true; | ||
}; | ||
} | ||
export declare type ExpectTypeOf<Actual, Options extends { | ||
positive: boolean; | ||
}> = (Options['positive'] extends true ? PositiveExpectTypeOf<Actual> : NegativeExpectTypeOf<Actual>); | ||
export interface BaseExpectTypeOf<Actual, Options extends { | ||
positive: boolean; | ||
}> { | ||
toBeAny: Scolder<ExpectAny<Actual>, Options>; | ||
toBeUnknown: Scolder<ExpectUnknown<Actual>, Options>; | ||
toBeNever: Scolder<ExpectNever<Actual>, Options>; | ||
toBeFunction: Scolder<ExpectFunction<Actual>, Options>; | ||
toBeObject: Scolder<ExpectObject<Actual>, Options>; | ||
toBeArray: Scolder<ExpectArray<Actual>, Options>; | ||
toBeNumber: Scolder<ExpectNumber<Actual>, Options>; | ||
toBeString: Scolder<ExpectString<Actual>, Options>; | ||
toBeBoolean: Scolder<ExpectBoolean<Actual>, Options>; | ||
toBeVoid: Scolder<ExpectVoid<Actual>, Options>; | ||
toBeSymbol: Scolder<ExpectSymbol<Actual>, Options>; | ||
toBeNull: Scolder<ExpectNull<Actual>, Options>; | ||
toBeUndefined: Scolder<ExpectUndefined<Actual>, Options>; | ||
toBeNullable: Scolder<ExpectNullable<Actual>, Options>; | ||
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>, Options['positive']>) => K extends keyof Actual ? ExpectTypeOf<Actual[K], Options> : true; | ||
extract: <V>(v?: V) => ExpectTypeOf<Extract<Actual, V>, Options>; | ||
@@ -118,10 +239,2 @@ exclude: <V>(v?: V) => ExpectTypeOf<Exclude<Actual, V>, Options>; | ||
asserts: Actual extends (v: any, ...args: any[]) => asserts v is infer T ? unknown extends T ? never : ExpectTypeOf<T, Options> : never; | ||
branded: Omit<ExpectTypeOf<Actual, { | ||
positive: Options['positive']; | ||
branded: true; | ||
}>, 'branded'>; | ||
not: Omit<ExpectTypeOf<Actual, { | ||
positive: Not<Options['positive']>; | ||
branded: Options['branded']; | ||
}>, 'not'>; | ||
} | ||
@@ -128,0 +241,0 @@ export declare type _ExpectTypeOf = { |
@@ -5,2 +5,3 @@ "use strict"; | ||
const secret = Symbol('secret'); | ||
const error = Symbol('error'); | ||
const fn = () => true; | ||
@@ -7,0 +8,0 @@ /** |
{ | ||
"name": "expect-type", | ||
"version": "0.16.0", | ||
"version": "0.17.0-1", | ||
"engines": { | ||
@@ -43,3 +43,3 @@ "node": ">=12.0.0" | ||
"jest": "28.1.3", | ||
"np": "8.0.1", | ||
"np": "^8.0.4", | ||
"strip-ansi": "6.0.1", | ||
@@ -46,0 +46,0 @@ "ts-jest": "28.0.8", |
113
README.md
@@ -35,2 +35,5 @@ # expect-type | ||
- [Features](#features) | ||
- [Where is `.toExtend`?](#where-is-toextend) | ||
- [Use internal type helpers at your own risk](#use-internal-type-helpers-at-your-own-risk) | ||
- [Error messages](#error-messages) | ||
- [Within test frameworks](#within-test-frameworks) | ||
@@ -85,8 +88,17 @@ - [Jest & `eslint-plugin-jest`](#jest--eslint-plugin-jest) | ||
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: | ||
@@ -285,2 +297,11 @@ | ||
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: | ||
@@ -441,3 +462,3 @@ | ||
To workaround, you can use a mapped type: | ||
To workaround for simple cases, you can use a mapped type: | ||
@@ -449,8 +470,92 @@ ```typescript | ||
``` | ||
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. | ||
### Use internal type helpers at your own risk | ||
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`. Nothing is stopping you using these beyond the following 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:9:99 - 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\\"'. | ||
9 expectTypeOf({a: 1}).toEqualTypeOf<{a: string}>() | ||
~~~~~~~~~~~ | ||
``` | ||
Not 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. | ||
### 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()`. | ||
@@ -457,0 +562,0 @@ To remove this warning, configure the ESlint rule to consider `expectTypeOf` as an assertion: |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
50625
344
614