Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #43 from silvermine/strict-union
feat: add StrictUnion
- Loading branch information
Showing
3 changed files
with
124 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
}); | ||
|
||
}); |