tsdjs / tsd

Check TypeScript type definitions
MIT License
2.36k stars 68 forks source link

Add Jest like API #168

Open skarab42 opened 1 year ago

skarab42 commented 1 year ago

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

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 commented 1 year ago

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 commented 1 year ago

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

sindresorhus commented 1 year ago

decoupling the expected type from the type to be compared makes the bug https://github.com/SamVerschueren/tsd/issues/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 commented 1 year ago

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 commented 1 year ago

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.