Check out a free preview of the full Enterprise TypeScript course:
The "Tests for Types" Lesson is part of the full, Enterprise TypeScript course featured in this preview video. Here's what you'd learn in this lesson:

Mike compares positive and negative test cases for types in TypeScript. While positive test cases are easy to write, negative test cases are important to catch mistakes that should not be compilable. Two tools for writing negative test cases are introduced: DTS Lint and TSD.

Get Unlimited Access Now

Transcript from the "Tests for Types" Lesson

>> Mike North: The last topic I wanna talk to you about is tests for types. Specifically, I want to focus on writing negative test cases. It's quite easy to write positive test cases for type information. And you could argue that the jest tests we've already been writing are positive test cases.

[00:00:18] They have to compile their TypeScript files. And so we make sure that the things that should compile properly do compile properly. But what about cases where we wanna say, here's an example of something that the types should catch for me. This is a mistake that should not be compilable.

[00:00:39] It's important that we we have those kinds of tests as well, because sometimes bugs can happen where types turn into enemies inappropriately. We would say that's like an inappropriate widening of types. You lose your type safety and of course all your tests compile like ne's are perfectly happy to let a lot of things compile, too many things.

[00:01:00] So we'll go through an example of how we can write some negative test cases. There are two tools that I like using for this, for different reasons although one is sort of very established and not doing much and the other is catching up. So it may be that the second tool I'm gonna talk to you about is sort of the one that ends up winning.

[00:01:25] But the first tool is called DTS lint. And DTS is like a declaration file. So where this is used most is in the definitely typed repo. All the tests for definitely typed types are written to run in DTS lint. So we're gonna go ahead and install it, we still can use it locally and there are some benefits but that'll be the first thing that we try.

[00:01:52] And the benefits there are that it automatically downloads different compiler versions and tests them against your types. So you could say, for example, this should work in TypeScript version 3.4, and it'll check out version 3.4, 3.5, .6, .7, .8, .9. All the 4.x releases, all the stable 5.x releases, and it'll run your test file against each one of those to give you confidence that if you're publishing a library and you wanna make sure that whole range continues to work, definitely typed is great for that.

[00:02:33] Definitely typed also automatically tests against nightly TypeScript builds. So it's a great way to know if there's sort of breakage headed on its way to you. I have filed a lot of bug reports against TypeScript that before they ever affected the apps that I was caring for. And having a nightly build to test against is great.

[00:02:55] That's exactly what Microsoft wants you to do. It's part of keeping that feedback loop nice and tight. Although I said it isn't doing much, meaning new features haven't been added in a long time, Microsoft uses this heavily. And it's so ingrained and definitely types that I really can't imagine how they would stop making sure it continues to work.

>> Mike North: So, concerns with this, it's very particular of how it's set up. When you're testing, you end up really making assertions against stringified types. So sometimes you're really writing assertions in comments and even just getting the spacing between things wrong, which would not really be important in code, it'll still fail the test.

[00:03:43] So you have to understand what its conventions are and align with them.
>> Mike North: All right, the other library that I like using is tsd. And let's take a look at that. So tsd is a much more modern project. You can see there's a lot of active contributors to it.

[00:04:02] And the way they would help you, they would have you write your tests are really using types, not magic comets where you are sort of asserting that the stringfield type matches a particular thing but you can use generics to write your assertion. You are expecting the return type of concat to be of type string.

[00:04:27] So these types are easier to maintain, they're a lot more flexible, they're less likely to change from version to version. When we're talking about stringified types, sometimes TypeScript it'll just change how they present a particular type. Where one release they might, when you say, the key of this interface, they might enumerate all of the fields and in the next they'll just say, I'm just gonna call this key of the interface name, and that that changes how you'd have to write your assertion.

[00:04:59] But this is much more flexible and does not require the stringfield type kind of thing. So there are a couple of different assertions you can make, like expect assignable, expect error, and it's quite well thought through.
>> Mike North: You can even assert that things should remain deprecated so you can catch if somebody tries to remove a deprecation tag on a particular thing.

[00:05:28] This is a great thing for asserting that something handles a value exhaustively, it was returning and never, that means there's nothing left, a lot of valuable things here. So let's take a look at both. So, we wrote this function, isTypedArray. It was kind of an interesting type guard, and we wanna make sure that it works the way it's supposed to work in different scenarios.

[00:05:56] So, the first thing we're gonna have to do is create a new folder in our tests folder. So we're gonna call it types/dtslint, and we'll create a bunch of files in there, these files above. So I'm gonna go and make that folder and chat tests.
>> Mike North: Make sure.

>> Mike North: Some of this is gonna seem a little bit funky, but that's just the way it is with this package. This is like this thing.
>> Mike North: Chat tests typesdtslint. Okay, there we go. So we're gonna get our tsconfig.json. There's the contents of it. I'm gonna grab that and move it over.

[00:06:53] We have to fix the thing or two, check it out together. Okay, just making sure that this is the right tsconfig.json. I'm gonna Command click on it and we get here. That looks good.
>> Mike North: And then let's make sure this source folder works. So that would take us up one up one.

[00:07:16] I think we need one more hop. So this would take us into tests. Nope, this should be good.
>> Mike North: All right, one more file we have to add in there, tslint.json. This is a linter that was originally written for TypeScript. It predates ESLint having great TypeScript support. It is far less popular than ESLint right now.

[00:07:42] I would advise that you use ESLint. But DTSLint is built on top of this. You don't get to make a choice here.
>> Mike North: So TSLint.
>> Mike North: Great,
>> Mike North: So now what we're gonna do is we're gonna create a declaration file.
>> Mike North: Now I'm actually gonna open up a DefinitelyTyped repo.

[00:08:17] It's a great reference to look at when you're creating one of these.
>> Mike North: So let's just do definitely. Chai is fine.
>> Mike North: And we'll look up at the top of this file.
>> Mike North: Actually, we don't even need that special stuff at the top of the file anymore. Let's look at our package.json here and make sure that we got everything we need.

[00:08:43] Probably don't need the owners.
>> Mike North: Let's look in this folder real quick.
>> Mike North: Just gonna check these out, that seems fine, tsconfig.
>> Mike North: Seems fine, enumerate files here, and here's what the tests look like. So let's give this a try. We can maybe take advantage of references that we have already available to us in the outer tests folder.

[00:09:25] So what we want here is something like,
>> Mike North: Like this and,
>> Mike North: type-guards.
>> Mike North: isTypedArray.
>> Mike North: Great, we have the function here.
>> Mike North: I don't actually think we'll need the node types in here, so we can probably just have that be the first line. Okay, and now what I wanna do is I'm actually gonna have to export this like that.

>> Mike North: I told you we'd be doing some wacky stuff here.
>> Mike North: Okay, now let's look back at the tests for chai. So they've got chai-tests. So we're gonna do chat-tests.d.ts.
>> Mike North: Part of why we're doing this is dtslint wants sort of a root declaration file that represents the library.

[00:10:44] So this is still here. We're effectively re-exporting that symbol. Dtsflint wants to regard something as sort of like where it can find the types, like within this types-dtsflint folder.
>> Mike North: Let me see if it's in the project.
>> Mike North: Okay, it's not here, so we're gonna have to do.

[00:11:11] Make sure I know where I am. Yarn add -D dtslint.
>> Mike North: Cool.
>> Mike North: So what it's doing now, now that I ran this command, yarn dtslint and then the path to that folder we created, you can see in the background it's downloading a bunch of different TypeScript versions.

[00:11:43] It's gonna hold these in a cache called .dts, which is great, because if you use this across a bunch of projects, it's not storing them on a per project basis. If you use GitHub Actions, this would be a great thing to cache. And now, let's see, it's taking issue with a bunch of different stuff that's going on.

[00:12:02] Errors in TypeScript 3 for external dependencies.
>> Mike North: Why don't we just do this? Why don't we just drop the type in here? I would normally add this into the project after we've sort of swatted away at a lot of the strictness work, gone through that full list of different things.

[00:12:25] But we can do just fine this way.
>> Mike North: So there's our type.
>> Mike North: This is just quite a bit of work that we would have to do between where we're at and being able to regard,
>> Mike North: Being able to get TSLint happy with this. So isTypedArray, let's run this again.

>> Mike North: Still giving us those errors?
>> Mike North: No, we're not gonna include source. That's how we can stop that. It's just a totally different linter with totally different rules. Kinda have to kick this thing a little bit until you get it to where it wants to help you out.

>> Mike North: Okay, this is better. So it's saying this file should not end with a blank line. Okay, that's pretty picky. File should end with a new line.
>> Mike North: Just wanna get it into a good baseline, then we can write a test.
>> Mike North: Did I not save things?
>> Mike North: New line here.

>> Mike North: I'll just start disabling rules if this bugs us too much longer.
>> Mike North: Okay, this threw an error, we're going to disable the rule.
>> Mike North: This is gonna convince all you all to use TSD cuz the setup for that is night and day compared to this. Okay, we're in a working state now.

[00:14:57] So in our test, our chat-test, we could say,
>> Mike North: isTypedArray, and we're going to pass in an unknown array, and here we'll say toTest: unknown.
>> Mike North: Something like that.
>> Mike North: Sorry, arrow functions index here.
>> Mike North: Let's break this out as a separate value.
>> Mike North: Something like that.
>> Mike North: Fine, if it really wants me to export it.

[00:16:06] All right, here's the numberGuard. isTypedArray. Statements are not allowed in ambient contexts.
>> Mike North: I wonder if I,
>> Mike North: Else, I'm still here. This should be a ts file, not a dts file.
>> Mike North: That's important.
>> Mike North: And then this is gonna complain about not being initialized, which is fine.

[00:16:43] And we can just say numberGuard.
>> Mike North: Returns that, and we'll implement it, typeof, there you go, toTest === 'number'. So isTypedArray, we can see it returns this type, arg is number array. So we could do,
>> Mike North: Something like that, and let's run dtslint, see if it's happy, it's probably.

>> Mike North: We gotta return this.
>> Mike North: I'll cancel that, cuz it won't be successful there.
>> Mike North: So I expect this to pass.
>> Mike North: Consecutive blank lines are forbidden. This is an example of not having too many opinions. Missing whitespace on line 8, comment must start with a space, file should end with a new line.

[00:18:06] I would recommend turning a lot of that stuff off, cuz we're playing whack-a-mole here.
>> Mike North: Okay, TypeScript@5.3 expected type to be: arg is number[] and got: boolean.
>> Mike North: So this is an example of where the tooltip we're getting is quite different from what dtslint is seeing.
>> Mike North: Where would the last whitespace be, missing whitespace, 17:7.

>> Speaker 2: After the comma?
>> Mike North: Yes, that would be it. There we go, finally. If we had this as something different, you would see. I mean, this is an example of a failure where it's like, I got this, it should have been this. So you could write a bunch of these with the special ExpectType comments here, and that would work nicely.

[00:19:08] I mean, if it would work, once you wrestle it into submission. We're now gonna install tsd, and it's comparatively much easier to set up and use. And we're gonna add some type-based assertions just sort of at the bottom of one of our just tests. The downside here, of course, is we're not gonna get the benefit of testing our types against multiple different compiler versions.

[00:19:29] But it's perfectly good, especially if you're dealing with an app. And you don't need to be worried about supporting different range of versions, it can do just fine. So I'm gonna make a quick git commit.
>> Mike North: Great, and then we're gonna add a package to chat, yarn add -D tsd.

>> Mike North: And we're gonna go back to our tests, in the utils folder, for example. We could just write, let's deal with this date test here. We can import {expectType} from 'tsd'. And then here we can write expectType<string>. And then we can pass in our formatTimestamp function here, just getting rid of the extra syntax at the end, another parenthesis.

[00:20:28] And we expect this to be a string, if it were a number we expected, you can see that this will light up. And so this serves as a nice assertion to make sure that things work the way you would expect them to. You'd usually use this to get a lot of confidence around utility types or type guards or things like that, where a lot of generics are floating around.

[00:20:52] And again, the point here is to catch things that should not compile. Your just tests, the conventional tests that you would write won't really help you with that.