Reputation: 637
I'm working on extending a bunch of typescript types into a utility library as a personal "do I really understand Typescript?" project.
I'm just having a LOT of trouble figuring out how to unit test the damn thing.
The problem is that I want to test that when I pass the wrong type into a function that calls for a type, that the TSC will catch it. Which it does. Brilliantly. But there's a teensy little flaw with that, in that the unit testing library I use (Jest) will throw an error (the desired behavior) and then refuse to run the rest of the test suite because it threw an error. I can't even even use "will throw error" because it's not a Javascript error but a Typescript error.
Here's the code I have so far. I'm using Jest, but my solution doesn't need to be Jest specific, I have a funny feeling I might need some sort of specialized testing library.
// src/Either/Either.ts
export type Either<Base, Either, Or> = (Base & Either) | (Base & Or);
// src/either/Either.spec.ts
import { Either } from "./Either";
interface Pet {
name: string;
}
interface Dog {
says: "awoo" | "ruff" | "yip" | "bork" | "bark" | "grr";
willFetch: true;
}
interface Cat {
says: "meow" | "hiss";
willFetch: false;
}
describe("Either<Base, Either, Or>", () => {
it("allows one extension OR the other, but not both, and not neither", () => {
type CommonPet = Either<Pet, Dog, Cat>;
const whatAmI = (pet: CommonPet): "dog" | "cat" => {
if (pet.willFetch === true) {
return "dog";
}
return "cat";
};
expect(whatAmI({ name: "Rex", willFetch: true, says: "awoo" })).toBe("dog");
expect(whatAmI({ name: "Psycho", says: "hiss", willFetch: false })).toBe(
"cat"
);
// these lines will throw a typescript error (desired)
/*
const freak = whatAmI({ name: "Freak", says: "meow", willFetch: true });
const uncommonPet = whatAmI({ name: "Nagini", says: 'hiss', willFetch: false, isSnake: true });
*/
// but how to I tell Jest that I WANT it to throw a typescript error?
interface AristoCat {
knowsWhereItsAt: boolean;
}
interface JellicleCat {
nightmareFuel: number;
}
type MusicalCat = Either<Pet & Cat, AristoCat, JellicleCat>;
const isGood = (cat: MusicalCat): boolean | never => {
if ("knowsWhereItsAt" in cat) {
return true;
}
if (cat.nightmareFuel > 0) {
throw new Error("NOOOOOO!");
}
return false;
};
expect(
isGood({
name: "Duchess",
says: "meow",
willFetch: false,
knowsWhereItsAt: true
})
).toBe(true);
expect(
isGood({
name: "MacCavity (stage)",
willFetch: false,
says: "meow",
nightmareFuel: 0
})
).toBe(false);
const filmIt = () =>
isGood({
name: "MacCavity",
willFetch: false,
says: "meow",
nightmareFuel: 9001
});
expect(filmIt).toThrowErrorMatchingInlineSnapshot(`"NOOOOOO!"`);
// again, we expect this to throw in TYPESCRIPT because it has BOTH.
/*
const dreamIHadOnAcid = isGood({
name: 'Cheshire',
says: 'meow',
willFetch: false,
knowsWhereItsAt: true,
nightmareFuel: 5/7
})
*/
// again, we expect this to throw in TYPESCRIPT because it has NEITHER.
/*
const dreamIHadOnAcid = isGood({
name: 'Tardar Sauce',
says: 'meow',
willFetch: false,
})
*/
});
});
-- EDIT:
There's an additional difficulty. In that I'm not sure my Either type is working the way it should (or if it is, that the compiler can't pick it up.)
I'm using the Typescript playground for this...
export type Either<Base, Either, Or> = (Base & Either) | (Base & Or);
type Pet = {
name: string;
says?: string;
}
type DogExtension = {
playHours: number;
}
type CatExtension = {
sleepHours: number;
}
type CommonPet = Either<Pet, DogExtension, CatExtension>;
const dog: CommonPet = { name: "rex", says: "awoo", playHours: 5 };
const cat: CommonPet = { name: "manx", says: "meow", sleepHours: 14 };
const testPet = (testName: string, pet: CommonPet): CommonPet => {
return pet
};
testPet("dog should pass", dog);
testPet("cat should pass", cat);
testPet("catdog should not pass", { ...dog, ...cat }); /* should throw a type error, but doesn't */
This may be a bug in the typescript compiler... and an easy one to make. Dog is a CommonPet, Cat is a CommonPet, so why wouldn't {...dog, ...cat} be a common pet? Unless - the far more likely solution, my Either =(T&E) | (T&O) just don't work the way I thought it would.
Upvotes: 5
Views: 2823
Reputation: 1379
I did the same as you, to learn TypeScript. I have a bunch of unit tests here. The library itself does sort of what you want to achieve. So I recommend that you start writing your strict Equals
type, to check your type results.
Upvotes: 0
Reputation: 168
I faced this problem before and managed to get it working with dtslint. I found these tests really valuable for writing libraries that include a lot of generic type logic because I constantly got into scenarios where fixing one thing lead to types being inferred incorrectly elsewhere.
Here's the repo where I managed to get it working. Specifically, this file contains one of the tests. The type tests can be run with yarn type-check
or npm run type-check
.
When something fails, you'll get an error like this:
Error: /good-form/types/Field.types.ts:19:3
ERROR: 19:3 expect TypeScript@local expected type to be:
number
got:
string
Upvotes: 2