Skip to content

Commit d63eef9

Browse files
committed
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): microsoft/TypeScript#37762
1 parent 0128c81 commit d63eef9

File tree

3 files changed

+63
-2
lines changed

3 files changed

+63
-2
lines changed

src/types/objects.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ export type CombineObjects<T extends object, U extends object> = ObjectType<T &
3434
* @returns `T[K]` if the key exists, `never` otherwise
3535
*/
3636
export type GetKey<T, K extends keyof any> = K extends keyof T ? T[K] : never;
37+
/**
38+
* Like `GetKey`, but returns `unknown` if the key is not present on the object.
39+
* @param T Object to get values from
40+
* @param K Key to query object for value
41+
* @returns `T[K]` if the key exists, `unknown` otherwise
42+
*/
43+
export type TryKey<T, K extends keyof any> = K extends keyof T ? T[K]: unknown;
44+
/**
45+
* Takes two objects and returns their element-wise intersection.
46+
* *Note*: this removes any key-level information, such as optional or readonly keys.
47+
* @param T First object to be intersected
48+
* @param U Second object to be intersected
49+
* @returns element-wise `T` & `U` cleaned up to look like flat object to VSCode
50+
*/
51+
export type ElementwiseIntersect<T extends object, U extends object> = {
52+
[k in (keyof T | keyof U)]: TryKey<T, k> & TryKey<U, k>;
53+
};
3754

3855
// ----
3956
// Keys

test/objects/CombineObjects.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { CombineObjects } from '../../src';
55

66
test('Can combine two objects (without pesky & in vscode)', t => {
77
type a = { x: number, y: 'hi' };
8-
type b = { z: number, y: 'there' };
8+
type b = { z: number };
99

1010
type got = CombineObjects<a, b>;
1111
type expected = {
1212
x: number,
13-
y: 'hi' & 'there',
13+
y: 'hi',
1414
z: number
1515
};
1616

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import test from 'ava';
2+
import { assert } from '../helpers/assert';
3+
4+
import { ElementwiseIntersect } from '../../src';
5+
6+
test('Can combine two objects elementwise', t => {
7+
type a = { x: number, y: 'hi' };
8+
type b = { z: number, y: 'there' };
9+
10+
type got = ElementwiseIntersect<a, b>;
11+
type expected = {
12+
x: number,
13+
y: 'hi' & 'there',
14+
z: number,
15+
};
16+
17+
assert<got, expected>(t);
18+
assert<expected, got>(t);
19+
});
20+
21+
test('Can combine two objects with private members elementwise', t => {
22+
class A {
23+
a: number = 1;
24+
private x: number = 2;
25+
y: 'hi' = 'hi';
26+
private z: 'hey' = 'hey';
27+
}
28+
29+
class B {
30+
a: 22 = 22;
31+
private x: number = 2;
32+
y: 'there' = 'there';
33+
private z: 'friend' = 'friend';
34+
}
35+
36+
type got = ElementwiseIntersect<A, B>;
37+
type expected = {
38+
a: 22,
39+
y: 'hi' & 'there',
40+
};
41+
42+
assert<got, expected>(t);
43+
assert<expected, got>(t);
44+
});

0 commit comments

Comments
 (0)