Skip to content

Commit 0043ba1

Browse files
authored
Allow weak type detection for intersection sources (#26668)
Previously, intersections were only allowed as targets, but this was just an artifact of the original implementation, which operated inside the structural part of isRelatedTo. Removing this restriction catches subtle bugs in React user code, where a function named `create` returns a mapped type whose types are all branded numbers. The display of these properties, for some original type `T`, is not `number & { __ }` but the much-less-obvious `RegisteredStyle<T>`.
1 parent bdb7c35 commit 0043ba1

File tree

6 files changed

+207
-1
lines changed

6 files changed

+207
-1
lines changed

src/compiler/checker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11178,7 +11178,7 @@ namespace ts {
1117811178
}
1117911179

1118011180
if (relation !== comparableRelation &&
11181-
!(source.flags & TypeFlags.UnionOrIntersection) &&
11181+
!(source.flags & TypeFlags.Union) &&
1118211182
!(target.flags & TypeFlags.Union) &&
1118311183
!isIntersectionConstituent &&
1118411184
source !== globalObjectType &&
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
tests/cases/conformance/types/intersection/intersectionAsWeakTypeSource.ts(8,7): error TS2559: Type 'XY' has no properties in common with type 'Z'.
2+
tests/cases/conformance/types/intersection/intersectionAsWeakTypeSource.ts(18,7): error TS2322: Type 'Brand<{ view: number; styleMedia: string; }>' is not assignable to type 'ViewStyle'.
3+
Property 'view' is missing in type 'Number & { __brand: { view: number; styleMedia: string; }; }'.
4+
5+
6+
==== tests/cases/conformance/types/intersection/intersectionAsWeakTypeSource.ts (2 errors) ====
7+
interface X { x: string }
8+
interface Y { y: number }
9+
interface Z { z?: boolean }
10+
11+
type XY = X & Y;
12+
const xy: XY = {x: 'x', y: 10};
13+
14+
const z1: Z = xy; // error, {xy} doesn't overlap with {z}
15+
~~
16+
!!! error TS2559: Type 'XY' has no properties in common with type 'Z'.
17+
18+
19+
interface ViewStyle {
20+
view: number
21+
styleMedia: string
22+
}
23+
type Brand<T> = number & { __brand: T }
24+
declare function create<T extends { [s: string]: ViewStyle }>(styles: T): { [P in keyof T]: Brand<T[P]> };
25+
const wrapped = create({ first: { view: 0, styleMedia: "???" } });
26+
const vs: ViewStyle = wrapped.first // error, first is a branded number
27+
~~
28+
!!! error TS2322: Type 'Brand<{ view: number; styleMedia: string; }>' is not assignable to type 'ViewStyle'.
29+
!!! error TS2322: Property 'view' is missing in type 'Number & { __brand: { view: number; styleMedia: string; }; }'.
30+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//// [intersectionAsWeakTypeSource.ts]
2+
interface X { x: string }
3+
interface Y { y: number }
4+
interface Z { z?: boolean }
5+
6+
type XY = X & Y;
7+
const xy: XY = {x: 'x', y: 10};
8+
9+
const z1: Z = xy; // error, {xy} doesn't overlap with {z}
10+
11+
12+
interface ViewStyle {
13+
view: number
14+
styleMedia: string
15+
}
16+
type Brand<T> = number & { __brand: T }
17+
declare function create<T extends { [s: string]: ViewStyle }>(styles: T): { [P in keyof T]: Brand<T[P]> };
18+
const wrapped = create({ first: { view: 0, styleMedia: "???" } });
19+
const vs: ViewStyle = wrapped.first // error, first is a branded number
20+
21+
22+
//// [intersectionAsWeakTypeSource.js]
23+
var xy = { x: 'x', y: 10 };
24+
var z1 = xy; // error, {xy} doesn't overlap with {z}
25+
var wrapped = create({ first: { view: 0, styleMedia: "???" } });
26+
var vs = wrapped.first; // error, first is a branded number
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
=== tests/cases/conformance/types/intersection/intersectionAsWeakTypeSource.ts ===
2+
interface X { x: string }
3+
>X : Symbol(X, Decl(intersectionAsWeakTypeSource.ts, 0, 0))
4+
>x : Symbol(X.x, Decl(intersectionAsWeakTypeSource.ts, 0, 13))
5+
6+
interface Y { y: number }
7+
>Y : Symbol(Y, Decl(intersectionAsWeakTypeSource.ts, 0, 25))
8+
>y : Symbol(Y.y, Decl(intersectionAsWeakTypeSource.ts, 1, 13))
9+
10+
interface Z { z?: boolean }
11+
>Z : Symbol(Z, Decl(intersectionAsWeakTypeSource.ts, 1, 25))
12+
>z : Symbol(Z.z, Decl(intersectionAsWeakTypeSource.ts, 2, 13))
13+
14+
type XY = X & Y;
15+
>XY : Symbol(XY, Decl(intersectionAsWeakTypeSource.ts, 2, 27))
16+
>X : Symbol(X, Decl(intersectionAsWeakTypeSource.ts, 0, 0))
17+
>Y : Symbol(Y, Decl(intersectionAsWeakTypeSource.ts, 0, 25))
18+
19+
const xy: XY = {x: 'x', y: 10};
20+
>xy : Symbol(xy, Decl(intersectionAsWeakTypeSource.ts, 5, 5))
21+
>XY : Symbol(XY, Decl(intersectionAsWeakTypeSource.ts, 2, 27))
22+
>x : Symbol(x, Decl(intersectionAsWeakTypeSource.ts, 5, 16))
23+
>y : Symbol(y, Decl(intersectionAsWeakTypeSource.ts, 5, 23))
24+
25+
const z1: Z = xy; // error, {xy} doesn't overlap with {z}
26+
>z1 : Symbol(z1, Decl(intersectionAsWeakTypeSource.ts, 7, 5))
27+
>Z : Symbol(Z, Decl(intersectionAsWeakTypeSource.ts, 1, 25))
28+
>xy : Symbol(xy, Decl(intersectionAsWeakTypeSource.ts, 5, 5))
29+
30+
31+
interface ViewStyle {
32+
>ViewStyle : Symbol(ViewStyle, Decl(intersectionAsWeakTypeSource.ts, 7, 17))
33+
34+
view: number
35+
>view : Symbol(ViewStyle.view, Decl(intersectionAsWeakTypeSource.ts, 10, 21))
36+
37+
styleMedia: string
38+
>styleMedia : Symbol(ViewStyle.styleMedia, Decl(intersectionAsWeakTypeSource.ts, 11, 16))
39+
}
40+
type Brand<T> = number & { __brand: T }
41+
>Brand : Symbol(Brand, Decl(intersectionAsWeakTypeSource.ts, 13, 1))
42+
>T : Symbol(T, Decl(intersectionAsWeakTypeSource.ts, 14, 11))
43+
>__brand : Symbol(__brand, Decl(intersectionAsWeakTypeSource.ts, 14, 26))
44+
>T : Symbol(T, Decl(intersectionAsWeakTypeSource.ts, 14, 11))
45+
46+
declare function create<T extends { [s: string]: ViewStyle }>(styles: T): { [P in keyof T]: Brand<T[P]> };
47+
>create : Symbol(create, Decl(intersectionAsWeakTypeSource.ts, 14, 39))
48+
>T : Symbol(T, Decl(intersectionAsWeakTypeSource.ts, 15, 24))
49+
>s : Symbol(s, Decl(intersectionAsWeakTypeSource.ts, 15, 37))
50+
>ViewStyle : Symbol(ViewStyle, Decl(intersectionAsWeakTypeSource.ts, 7, 17))
51+
>styles : Symbol(styles, Decl(intersectionAsWeakTypeSource.ts, 15, 62))
52+
>T : Symbol(T, Decl(intersectionAsWeakTypeSource.ts, 15, 24))
53+
>P : Symbol(P, Decl(intersectionAsWeakTypeSource.ts, 15, 77))
54+
>T : Symbol(T, Decl(intersectionAsWeakTypeSource.ts, 15, 24))
55+
>Brand : Symbol(Brand, Decl(intersectionAsWeakTypeSource.ts, 13, 1))
56+
>T : Symbol(T, Decl(intersectionAsWeakTypeSource.ts, 15, 24))
57+
>P : Symbol(P, Decl(intersectionAsWeakTypeSource.ts, 15, 77))
58+
59+
const wrapped = create({ first: { view: 0, styleMedia: "???" } });
60+
>wrapped : Symbol(wrapped, Decl(intersectionAsWeakTypeSource.ts, 16, 5))
61+
>create : Symbol(create, Decl(intersectionAsWeakTypeSource.ts, 14, 39))
62+
>first : Symbol(first, Decl(intersectionAsWeakTypeSource.ts, 16, 24))
63+
>view : Symbol(view, Decl(intersectionAsWeakTypeSource.ts, 16, 33))
64+
>styleMedia : Symbol(styleMedia, Decl(intersectionAsWeakTypeSource.ts, 16, 42))
65+
66+
const vs: ViewStyle = wrapped.first // error, first is a branded number
67+
>vs : Symbol(vs, Decl(intersectionAsWeakTypeSource.ts, 17, 5))
68+
>ViewStyle : Symbol(ViewStyle, Decl(intersectionAsWeakTypeSource.ts, 7, 17))
69+
>wrapped.first : Symbol(first, Decl(intersectionAsWeakTypeSource.ts, 16, 24))
70+
>wrapped : Symbol(wrapped, Decl(intersectionAsWeakTypeSource.ts, 16, 5))
71+
>first : Symbol(first, Decl(intersectionAsWeakTypeSource.ts, 16, 24))
72+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
=== tests/cases/conformance/types/intersection/intersectionAsWeakTypeSource.ts ===
2+
interface X { x: string }
3+
>x : string
4+
5+
interface Y { y: number }
6+
>y : number
7+
8+
interface Z { z?: boolean }
9+
>z : boolean
10+
11+
type XY = X & Y;
12+
>XY : XY
13+
14+
const xy: XY = {x: 'x', y: 10};
15+
>xy : XY
16+
>{x: 'x', y: 10} : { x: string; y: number; }
17+
>x : string
18+
>'x' : "x"
19+
>y : number
20+
>10 : 10
21+
22+
const z1: Z = xy; // error, {xy} doesn't overlap with {z}
23+
>z1 : Z
24+
>xy : XY
25+
26+
27+
interface ViewStyle {
28+
view: number
29+
>view : number
30+
31+
styleMedia: string
32+
>styleMedia : string
33+
}
34+
type Brand<T> = number & { __brand: T }
35+
>Brand : Brand<T>
36+
>__brand : T
37+
38+
declare function create<T extends { [s: string]: ViewStyle }>(styles: T): { [P in keyof T]: Brand<T[P]> };
39+
>create : <T extends { [s: string]: ViewStyle; }>(styles: T) => { [P in keyof T]: Brand<T[P]>; }
40+
>s : string
41+
>styles : T
42+
43+
const wrapped = create({ first: { view: 0, styleMedia: "???" } });
44+
>wrapped : { first: Brand<{ view: number; styleMedia: string; }>; }
45+
>create({ first: { view: 0, styleMedia: "???" } }) : { first: Brand<{ view: number; styleMedia: string; }>; }
46+
>create : <T extends { [s: string]: ViewStyle; }>(styles: T) => { [P in keyof T]: Brand<T[P]>; }
47+
>{ first: { view: 0, styleMedia: "???" } } : { first: { view: number; styleMedia: string; }; }
48+
>first : { view: number; styleMedia: string; }
49+
>{ view: 0, styleMedia: "???" } : { view: number; styleMedia: string; }
50+
>view : number
51+
>0 : 0
52+
>styleMedia : string
53+
>"???" : "???"
54+
55+
const vs: ViewStyle = wrapped.first // error, first is a branded number
56+
>vs : ViewStyle
57+
>wrapped.first : Brand<{ view: number; styleMedia: string; }>
58+
>wrapped : { first: Brand<{ view: number; styleMedia: string; }>; }
59+
>first : Brand<{ view: number; styleMedia: string; }>
60+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
interface X { x: string }
2+
interface Y { y: number }
3+
interface Z { z?: boolean }
4+
5+
type XY = X & Y;
6+
const xy: XY = {x: 'x', y: 10};
7+
8+
const z1: Z = xy; // error, {xy} doesn't overlap with {z}
9+
10+
11+
interface ViewStyle {
12+
view: number
13+
styleMedia: string
14+
}
15+
type Brand<T> = number & { __brand: T }
16+
declare function create<T extends { [s: string]: ViewStyle }>(styles: T): { [P in keyof T]: Brand<T[P]> };
17+
const wrapped = create({ first: { view: 0, styleMedia: "???" } });
18+
const vs: ViewStyle = wrapped.first // error, first is a branded number

0 commit comments

Comments
 (0)