From d63eef914ede8f4f681758dcd0ee108ef9b4e519 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 26 Apr 2020 06:21:06 -0600 Subject: [PATCH] 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); +});