Skip to content

Commit

Permalink
Merge pull request #43 from silvermine/strict-union
Browse files Browse the repository at this point in the history
feat: add StrictUnion
  • Loading branch information
onebytegone committed Dec 14, 2022
2 parents 19e5ba4 + 22080e1 commit 663b47b
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -4,6 +4,7 @@ export * from './types/Optional';
export * from './types/PropsWithType';
export * from './types/RequireDefined';
export * from './types/RequireOptional';
export * from './types/StrictUnion';
export * from './types/StringArrayOfStringsMap';
export * from './types/StringMap';
export * from './types/StringUnknownMap';
Expand Down
15 changes: 15 additions & 0 deletions src/types/StrictUnion.ts
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-type-alias */

type UnionKeys<T> = T extends unknown ? keyof T : never;
type InvalidKeys<K extends string | number | symbol> = { [P in K]? : never };
type StrictUnionHelper<T, TAll> = T extends unknown ? (T & InvalidKeys<Exclude<UnionKeys<TAll>, keyof T>>) : never;

/**
* A basic TypeScript union (e.g. A | B) results in a type containing the available
* properties form the provided types. When StrictUnion is used (e.g. StrictUnion<A | B>),
* the resulting type can only contain the properties from one of the types (e.g. all the
* properties from A, but none of the properties from B).
*
* See: https://github.com/microsoft/TypeScript/issues/20863#issuecomment-520551758
*/
export type StrictUnion<T> = StrictUnionHelper<T, T>;
108 changes: 108 additions & 0 deletions tests/types/StrictUnion.test.ts
@@ -0,0 +1,108 @@
import { expect } from 'chai';
import { StrictUnion } from '../../src/index';

describe('StrictUnion', () => {

// NOTE These tests do not really check much at runtime. The real value is in the type
// checking that happens by *using* the `StrictUnion` type in this test. If we break
// the definition of `StrictUnion` so that it no longer selects the correct
// properties, the TypeScript compiler will complain about this test.

it('unions two types as expected', () => {
interface Animal {
name: string;
}

interface Dog extends Animal {
furColor: string;
}

interface Fish extends Animal {
finCount: number;
}

// eslint-disable-next-line @typescript-eslint/no-type-alias
type Pet = StrictUnion<Dog | Fish>;

function adoptDog(dog: Dog): void {
expect(dog.name).to.be.a('string');
expect(dog.furColor).to.be.a('string');
}

function adoptFish(fish: Fish): void {
expect(fish.name).to.be.a('string');
expect(fish.finCount).to.be.a('number');
}

function adoptPet(pet: Pet): void {
expect(pet.name).to.be.a('string');
expect(pet.furColor).to.be.oneOf([ 'white', undefined ]);
expect(pet.finCount).to.be.oneOf([ undefined, 7.5 ]);
}

const spot: Pet = {
name: 'Spot',
furColor: 'white',
// Uncommenting `finCount` should result in "Types of property 'finCount' are
// incompatible." ts(2322)
// finCount: 0,
};

adoptPet(spot);
adoptDog(spot);
// Uncommenting `adoptFish` should result in "Types of property 'finCount' are
// incompatible." ts(2345)
// adoptFish(spot);

const fido: Dog = {
name: 'Fido',
furColor: 'white',
// Uncommenting `finCount` should result in "Types of property 'finCount' are
// incompatible." ts(2322)
// finCount: 0,
};

adoptPet(fido);
adoptDog(fido);
// Uncommenting `adoptFish` should result in "Types of property 'finCount' are
// incompatible." ts(2345)
// adoptFish(fido);

const nemo: Pet = {
name: 'Nemo',
// Uncommenting `furColor` should result in "Types of property 'furColor' are
// incompatible." ts(2322)
// furColor: 'n/a',
finCount: 7.5,
};

adoptPet(nemo);
// Uncommenting `adoptDog` should result in "Types of property 'furColor' are
// incompatible." ts(2345)
// adoptDog(nemo);
adoptFish(nemo);

const dory: Fish = {
name: 'Dory',
// Uncommenting `furColor` should result in "Types of property 'furColor' are
// incompatible." ts(2322)
// furColor: 'n/a',
finCount: 7.5,
};

adoptPet(dory);
// Uncommenting `adoptDog` should result in "Types of property 'furColor' are
// incompatible." ts(2345)
// adoptDog(dory);
adoptFish(dory);

// NOTE: leaving the following cases commented out as we don't have an automated way
// to test types. This package cannot ship with these uncommented as they report the
// error "X is declared but never used.ts(6196)".

// type PetName = Pet['name']; // Should be: "string"
// type PetFurColor = Pet['furColor']; // Should be: "string | undefined"
// type PetFinCount = Pet['finCount']; // Should be: "number | undefined"
});

});

0 comments on commit 663b47b

Please sign in to comment.