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

Add Jest like API #168

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft

Add Jest like API #168

wants to merge 17 commits into from

Conversation

skarab42
Copy link
Contributor

@skarab42 skarab42 commented Oct 22, 2022

This PR add a new assertion API.

This is a proof of concept, not all planned features are implemented yet, the rest will come soon enough, so stop me if this is something you don't want.

Planned features

  • (not.)identicalTo
  • (not.)assignableTo
  • (not.)subtypeOf
  • toThrowError with code and/or message matching
  • (not.)toBeDeprecated
  • (not.)toBeInternal
  • toHaveDocBlock
  • toHaveDocBlockTag
  • print
  • some helpers like toBeNever, toBeNull, ...
  • ...
import {assertType} from 'tsd';

declare const fooString: string;
declare const fooNumber: number;

// Pass
assertType<string>().identicalTo<string>();
assertType<string>().identicalTo(fooString);
assertType(fooString).identicalTo<string>();
assertType(fooString).identicalTo(fooString);

assertType<string>().not.identicalTo<number>();

// Fail
assertType<string>().identicalTo<number>();
assertType<number>().identicalTo(fooString);
assertType(fooString).identicalTo<number>();
assertType(fooString).identicalTo(fooNumber);

assertType<string>().not.identicalTo<string>();

assertType<'foo'>().identicalTo<string>();
assertType<'foo'>().identicalTo(fooString);
assertType('foo').identicalTo<string>();
assertType('foo').identicalTo(fooString);

In addition to being more descriptive, decoupling the expected type from the type to be compared makes the bug #142 with generics disappear by design.

declare const inferrable: <T = 'foo'>() => T;

// Pass
assertType<'foo'>().identicalTo(inferrable());
assertType(inferrable()).identicalTo('foo');

// Fail
assertType<string>().identicalTo(inferrable());
assertType(inferrable()).identicalTo(fooString);

@skarab42 skarab42 marked this pull request as draft October 23, 2022 10:57
@skarab42
Copy link
Contributor Author

Add toThrowError

type Test<T extends number> = T;

// Pass
assertType<Test<string>>().toThrowError();
assertType<Test<string>>().toThrowError(2344);
assertType<Test<string>>().toThrowError('does not satisfy the constraint');
assertType<Test<string>>().toThrowError(/^Type 'string'/);

// Fail
assertType<Test<number>>().toThrowError();
assertType<Test<string>>().toThrowError(2244);
assertType<Test<string>>().toThrowError('poes not satisfy the constraint');
assertType<Test<string>>().toThrowError(/Type 'string'$/);

@sindresorhus
Copy link
Collaborator

I'm personally not a big fan of the Jest API, but I'll leave that decision to Sam.

@sindresorhus
Copy link
Collaborator

decoupling the expected type from the type to be compared makes the bug #142 with generics disappear by design.

How does the problem disappear by design? If it fixes that issue, that is a big benefit of this PR.

@mmkal
Copy link

mmkal commented Oct 24, 2022

decoupling the expected type from the type to be compared makes the bug #142 with generics disappear by design.

How does the problem disappear by design? If it fixes that issue, that is a big benefit of this PR.

I think it does! (since the Actual type is already baked-into assertType(), so by the time the generic for Expected comes into play in .identicalTo(), the compiler won't alter the inferred typearg based on the expectation)

Would this be a replacement for the existing API? Seems confusing to have two very different ways of using tsd, one of which can tell you everything's ok when it's not. I ask partly because it might be nice to align APIs with expect-type here too - assertType(...).identicalTo(...) is equivalent to expectTypeOf(...).toEqualTypeOf(...), and assertType(...).assignableTo(...) is equivalent to expectTypeOf(...).toMatchTypeOf(...).

Since there are some users of each it could be nice to give them the same API (some people might care more about CLI error messages who'd go for tsd vs others who might want it to "just work" by running tsc who'd go for expect-type).

@skarab42
Copy link
Contributor Author

I think it does! (since the Actual type is already baked-into assertType(), so by the time the generic for Expected comes into play in .identicalTo(), the compiler won't alter the inferred typearg based on the expectation)

That's right. The thing with the current design is that we share the same parameter to test two potentially different types and due to the nature of TS generic inference this can produce unexpected results with optional parameter. As we provide the expected type it will be propagated to parameters that are not defined in the target type.

declare const inferrable: <T = 'SomeDefaultValue'>() => T

function expectType<Expected>(expected: Expected): void {}

expectType<number>(inferrable()); // 'T' is inferred from 'Expected' => number
expectType(inferrable<number>()); // 'Expected' is inferred from 'T' => number
expectType(inferrable()); // Since no parameter was provided 'Expected' is inferred from 'T' default value => 'SomeDefaultValue'

By splitting the expected type and the actual type and ensuring that the user never produces a generic and an argument at the same time the bug no longer occurs.

assertType(foo).assignableTo(bar);
assertType(foo).assignableTo<Bar>();
assertType<Foo>().assignableTo(bar);
assertType<Foo>().assignableTo<Bar>();

assertType<Foo>(bar).assignableTo<Bar>(); // Error: Do not provide a generic type and an argument value at the same time. 
assertType<Foo>().assignableTo(); // Error: A generic type or an argument value is required.

Would this be a replacement for the existing API? Seems confusing to have two very different ways of using tsd, one of which can tell you everything's ok when it's not.

This is a very good question. I think they can coexist without any problem for a while to allow a smooth transition. But that's a decision for @SamVerschueren to make (as long as he's happy with the new API).

I ask partly because it might be nice to align APIs with expect-type here too - assertType(...).identicalTo(...) is equivalent to expectTypeOf(...).toEqualTypeOf(...), and assertType(...).assignableTo(...) is equivalent to expectTypeOf(...).toMatchTypeOf(...).

I'm not convinced by this. The names of the methods are literally the names of the TS compiler and they do what they say they do. For example your toMatchTypeOf is rather a assignableTo or subtypeOf than assignableTo alone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants