From d63eef914ede8f4f681758dcd0ee108ef9b4e519 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 26 Apr 2020 06:21:06 -0600 Subject: [PATCH 1/3] feat: add element-wise intersection In typescript 3.9.0, the intersection between two objects will be `never` if the two objects have incompatible private members. This broke an implicit behavior that `CombineObjects` was taking advantage of; element-wise intersection. Because `CombineObjects` is intended to be a thin wrapper over the raw intersection type, I don't want its contract to deviate from the intersection. So instead I added the `ElementwiseIntersect` utility; which relies on the newly added `TryKey` utility. `TryKey` is just like `GetKey`, except it fails "silently" when the key does not exist. Specifically, `GetKey` returns `never` so that the resultant type is unusable and `TryKey` returns `unknown` which can be eliminated via intersection. Relevant PR from typescript which changed behavior of intersections (and broke the future-proofing test cases): https://github.com/microsoft/TypeScript/pull/37762 --- src/types/objects.ts | 17 +++++++++ test/objects/CombineObjects.test.ts | 4 +-- test/objects/ElementwiseIntersect.test.ts | 44 +++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 test/objects/ElementwiseIntersect.test.ts diff --git a/src/types/objects.ts b/src/types/objects.ts index 1c9f64d..36a0871 100644 --- a/src/types/objects.ts +++ b/src/types/objects.ts @@ -34,6 +34,23 @@ export type CombineObjects = ObjectType = K extends keyof T ? T[K] : never; +/** + * Like `GetKey`, but returns `unknown` if the key is not present on the object. + * @param T Object to get values from + * @param K Key to query object for value + * @returns `T[K]` if the key exists, `unknown` otherwise + */ +export type TryKey = K extends keyof T ? T[K]: unknown; +/** + * Takes two objects and returns their element-wise intersection. + * *Note*: this removes any key-level information, such as optional or readonly keys. + * @param T First object to be intersected + * @param U Second object to be intersected + * @returns element-wise `T` & `U` cleaned up to look like flat object to VSCode + */ +export type ElementwiseIntersect = { + [k in (keyof T | keyof U)]: TryKey & TryKey; +}; // ---- // Keys diff --git a/test/objects/CombineObjects.test.ts b/test/objects/CombineObjects.test.ts index 50dac54..f7da339 100644 --- a/test/objects/CombineObjects.test.ts +++ b/test/objects/CombineObjects.test.ts @@ -5,12 +5,12 @@ import { CombineObjects } from '../../src'; test('Can combine two objects (without pesky & in vscode)', t => { type a = { x: number, y: 'hi' }; - type b = { z: number, y: 'there' }; + type b = { z: number }; type got = CombineObjects; type expected = { x: number, - y: 'hi' & 'there', + y: 'hi', z: number }; diff --git a/test/objects/ElementwiseIntersect.test.ts b/test/objects/ElementwiseIntersect.test.ts new file mode 100644 index 0000000..937824e --- /dev/null +++ b/test/objects/ElementwiseIntersect.test.ts @@ -0,0 +1,44 @@ +import test from 'ava'; +import { assert } from '../helpers/assert'; + +import { ElementwiseIntersect } from '../../src'; + +test('Can combine two objects elementwise', t => { + type a = { x: number, y: 'hi' }; + type b = { z: number, y: 'there' }; + + type got = ElementwiseIntersect; + type expected = { + x: number, + y: 'hi' & 'there', + z: number, + }; + + assert(t); + assert(t); +}); + +test('Can combine two objects with private members elementwise', t => { + class A { + a: number = 1; + private x: number = 2; + y: 'hi' = 'hi'; + private z: 'hey' = 'hey'; + } + + class B { + a: 22 = 22; + private x: number = 2; + y: 'there' = 'there'; + private z: 'friend' = 'friend'; + } + + type got = ElementwiseIntersect; + type expected = { + a: 22, + y: 'hi' & 'there', + }; + + assert(t); + assert(t); +}); From 60c7a55c84baf55d44ccb22508d0fbe7035c533d Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 26 Apr 2020 06:27:10 -0600 Subject: [PATCH 2/3] test: add more TS versions to test --- scripts/testTsVersions.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/testTsVersions.sh b/scripts/testTsVersions.sh index d6c1bac..319d0ce 100644 --- a/scripts/testTsVersions.sh +++ b/scripts/testTsVersions.sh @@ -1,6 +1,7 @@ set -e -for v in 3.0.3 3.1.6 3.2.2 next; do +# highest patch version of each minor version +for v in 3.0.3 3.1.6 3.2.4 3.3.4000 3.4.5 3.5.3 3.6.5 3.7.5 3.8.3 next; do npm install --no-save typescript@$v npm test done From 777176d72743e22a8acfbca7e4d36447f6fa8ce7 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 26 Apr 2020 06:27:41 -0600 Subject: [PATCH 3/3] docs: generate documentation --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bdc861b..a02a8a1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ npm install --save-dev simplytyped **[Objects](#objects)** -[AllKeys](#allkeys) - [AllRequired](#allrequired) - [CombineObjects](#combineobjects) - [DeepPartial](#deeppartial) - [DeepReadonly](#deepreadonly) - [DiffKeys](#diffkeys) - [GetKey](#getkey) - [HasKey](#haskey) - [Intersect](#intersect) - [KeysByType](#keysbytype) - [Merge](#merge) - [ObjectKeys](#objectkeys) - [ObjectType](#objecttype) - [Omit](#omit) - [Optional](#optional) - [Overwrite](#overwrite) - [PlainObject](#plainobject) - [PureKeys](#purekeys) - [Required](#required) - [SharedKeys](#sharedkeys) - [StrictUnion](#strictunion) - [StringKeys](#stringkeys) - [TaggedObject](#taggedobject) - [UnionizeProperties](#unionizeproperties) - [UnionKeys](#unionkeys) +[AllKeys](#allkeys) - [AllRequired](#allrequired) - [CombineObjects](#combineobjects) - [DeepPartial](#deeppartial) - [DeepReadonly](#deepreadonly) - [DiffKeys](#diffkeys) - [ElementwiseIntersect](#elementwiseintersect) - [GetKey](#getkey) - [HasKey](#haskey) - [Intersect](#intersect) - [KeysByType](#keysbytype) - [Merge](#merge) - [ObjectKeys](#objectkeys) - [ObjectType](#objecttype) - [Omit](#omit) - [Optional](#optional) - [Overwrite](#overwrite) - [PlainObject](#plainobject) - [PureKeys](#purekeys) - [Required](#required) - [SharedKeys](#sharedkeys) - [StrictUnion](#strictunion) - [StringKeys](#stringkeys) - [TaggedObject](#taggedobject) - [TryKey](#trykey) - [UnionizeProperties](#unionizeproperties) - [UnionKeys](#unionkeys) **[Utils](#utils)** @@ -88,12 +88,12 @@ Useful for making extremely complex types look nice in VSCode. ```ts test('Can combine two objects (without pesky & in vscode)', t => { type a = { x: number, y: 'hi' }; - type b = { z: number, y: 'there' }; + type b = { z: number }; type got = CombineObjects; type expected = { x: number, - y: 'hi' & 'there', + y: 'hi', z: number }; @@ -231,6 +231,51 @@ test('Can get all keys that are different between objects', t => { }); ``` +### ElementwiseIntersect +Takes two objects and returns their element-wise intersection. +*Note*: this removes any key-level information, such as optional or readonly keys. +```ts +test('Can combine two objects elementwise', t => { + type a = { x: number, y: 'hi' }; + type b = { z: number, y: 'there' }; + + type got = ElementwiseIntersect; + type expected = { + x: number, + y: 'hi' & 'there', + z: number, + }; + + assert(t); + assert(t); +}); + +test('Can combine two objects with private members elementwise', t => { + class A { + a: number = 1; + private x: number = 2; + y: 'hi' = 'hi'; + private z: 'hey' = 'hey'; + } + + class B { + a: 22 = 22; + private x: number = 2; + y: 'there' = 'there'; + private z: 'friend' = 'friend'; + } + + type got = ElementwiseIntersect; + type expected = { + a: 22, + y: 'hi' & 'there', + }; + + assert(t); + assert(t); +}); +``` + ### GetKey Gets the value of specified property on any object without compile time error (`Property 'b' does not exist on type '{ a: string; }'.`) and the like. Returns `never` if the key is not on the object. @@ -471,6 +516,10 @@ For discriminated unions of objects, it is important to have a single "tag" prop Creates an object with each entry being tagged by the key defining that entry. +### TryKey +Like `GetKey`, but returns `unknown` if the key is not present on the object. + + ### UnionizeProperties Get a union of the properties of an object. ```ts