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

expectAssignable and expectNotAssignable not behaving correctly on arrays #199

Open
Yarmeli opened this issue Oct 31, 2023 · 4 comments
Open

Comments

@Yarmeli
Copy link

Yarmeli commented Oct 31, 2023

Hey

I noticed that the behaviour for expectAssignable and expectNotAssignable seems wrong for arrays

Here are a few examples;

✅ Works as expected

type myCustomType = {
  value: number[]
}

expectAssignable<myCustomType>({value: [1]})      // ✅ it passes
expectNotAssignable<myCustomType>({value: [1]})   // ✅ it fails as expected

❌ Fails

type myCustomType = {
  value: 1[] // Array of only 1's
}

expectAssignable<myCustomType>({value: [1]})      // ✅ it passes
expectNotAssignable<myCustomType>({value: [1]})   // ❌ it passes when it should fail

I saw that in #190 a suggested fix would be to use as const but this doesn't really work with arrays

type myCustomType = {
  value: 1[] // Array of only 1's
}

expectAssignable<myCustomType>({value: [1]} as const)      // ❌ it fails with error 
expectNotAssignable<myCustomType>({value: [1]} as const)   // ❌ it passes when it should fail

Any suggestions?

@mrazauskas
Copy link
Contributor

mrazauskas commented Nov 1, 2023

Did you try:

expectNotAssignable<myCustomType>({ value: [1 as const] });

@Yarmeli
Copy link
Author

Yarmeli commented Nov 1, 2023

I tried that and that approach works, but it kind of looks a bit meeh to do something like this:

expectNotAssignable<myCustomType>({ value: [1 as const, 1 as const, 1 as const] });

For a slightly longer type, I feel there are way too many as const needed to test and it isn't intuitive that as const should be used

type myCustomType = {
  simple: 1;
  optional: 2 | null;
  list: 3[];
}

expectAssignable<myCustomType>({
  simple: 1,
  optional: 2,
  list: [3, 3, 3]
});  // ✅ it passes, but should this have 'as const' as well?

expectNotAssignable<myCustomType>({
  simple: 1 as const,
  optional: 2 as const,
  list: [3 as const, 3 as const, 3 as const]
}); // ✅ only way to make it fails as expected

expectNotAssignable<myCustomType>({
  simple: 1, // not made 'as const'
  optional: 2 as const,
  list: [3 as const, 3 as const, 3 as const]
}); // ❌ it passes when it should fail

expectNotAssignable<myCustomType>({
  simple: 1 as const,
  optional: 2,  // not made 'as const'
  list: [3 as const, 3 as const, 3 as const]
}); // ❌ it passes when it should fail

expectNotAssignable<myCustomType>({
  simple: 1,  // not made 'as const'
  optional: 2,  // not made 'as const'
  list: [3 as const, 3 as const, 3 as const]
}); // ❌ it passes when it should fail

expectNotAssignable<myCustomType>({
  // make the full object 'as const'
  simple: 1,
  optional: 2, 
  list: [3, 3, 3]
} as const); // ❌ it passes when it should fail

Thoughts?

@mrazauskas
Copy link
Contributor

Thanks for asking. It is always interesting to talk about type testing.

Please note that I am not associated with tsd. It was used to test typings in Jest repo and I was contributing a lot of type tests. Limitations and issues of the library pushed me to create tsd-lite fork. It does not have the issue you are wrestling with, but at this moment I do not recommend migrating to tsd-lite.

It is going to be replaced by brand new type testing tool, which I will publish in about a week. It is written from scratch, the architecture is different and all that helps to solve limitations which tsd-lite inherited. For instance, it is enough to pass a CLI flag to test on several versions of TypeScript (see: jest-community/jest-runner-tsd#32 (comment)).


Back to your problem. What do you mean by “slightly longer type”? In this case { value: [1 as const] } is not a type, this is an expression. tsd is using TypeScript’s APIs to infer type of expression and compares it with the one you pass as type argument.

To check how type information is inferred, you can use hover information:

Screenshot 2023-11-02 at 11 30 00

But:

Screenshot 2023-11-02 at 11 29 33

That is just how TypeScript works. Use as const:

Screenshot 2023-11-02 at 11 29 09

All this is simple and obvious, of course. (Might be even intuitive, but that’s rather subjective.)


The problem is that tsds expectAssignable infers type of an expression like this:

Screenshot 2023-11-02 at 11 36 42

But expectNotAssignable infers type of the same expression like this:

Screenshot 2023-11-02 at 11 37 01

In a way, expectAssignable already knows what is expected. In my opinion, the behaviour of expectAssignable is broken. (By the way, it is the cause of #142 as well.) This is not a problem for tsd-lite. It was easy to fix.

Hope this explains the inconsistency of behaviour between expectAssignable and expectNotAssignable.

@Yarmeli
Copy link
Author

Yarmeli commented Nov 2, 2023

Gotcha, thanks for the explanation 😄

By “slightly longer type”, I meant this:

type myCustomType = {
  simple: 1;
  optional: 2 | null;
  list: 3[];
}

In the initial version where myCustomType only had a single key value, the idea to mark the array items as const wouldn't be too much of a hassle (i.e. { value: [1 as const] }), buuuut for the longer objects with more keys and at least one array, it becomes more of a hassle to declare every single thing as const since you can't just declare the full object as const as that would make the array readonly

// I'd prefer to do something like this (which doesn't work)
expectNotAssignable<myCustomType>({
  simple: 1,
  optional: 2, 
  list: [3, 3, 3]
} as const);


// Instead of marking everything as const
expectNotAssignable<myCustomType>({
  simple: 1 as const,
  optional: 2 as const,
  list: [3 as const, 3 as const, 3 as const]
}); 

I kind of expected expectNotAssignable to behave the same way as expectAssignable since we pass the type to each of them - instead of inferring the type as number, use the type that I gave you (if my type is number use that, if it is 1 use that instead)

Looking forward to tsd-lite

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

No branches or pull requests

2 participants