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.