Skip to content

Commit

Permalink
feat: add element-wise intersection
Browse files Browse the repository at this point in the history
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):
microsoft/TypeScript#37762
  • Loading branch information
andnp committed Apr 26, 2020
1 parent 0128c81 commit d63eef9
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 2 deletions.
17 changes: 17 additions & 0 deletions src/types/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ export type CombineObjects<T extends object, U extends object> = ObjectType<T &
* @returns `T[K]` if the key exists, `never` otherwise
*/
export type GetKey<T, K extends keyof any> = 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<T, K extends keyof any> = 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<T extends object, U extends object> = {
[k in (keyof T | keyof U)]: TryKey<T, k> & TryKey<U, k>;
};

// ----
// Keys
Expand Down
4 changes: 2 additions & 2 deletions test/objects/CombineObjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<a, b>;
type expected = {
x: number,
y: 'hi' & 'there',
y: 'hi',
z: number
};

Expand Down
44 changes: 44 additions & 0 deletions test/objects/ElementwiseIntersect.test.ts
Original file line number Diff line number Diff line change
@@ -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<a, b>;
type expected = {
x: number,
y: 'hi' & 'there',
z: number,
};

assert<got, expected>(t);
assert<expected, got>(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<A, B>;
type expected = {
a: 22,
y: 'hi' & 'there',
};

assert<got, expected>(t);
assert<expected, got>(t);
});

0 comments on commit d63eef9

Please sign in to comment.