Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve how intersections are classified as weak types #60889

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24323,18 +24323,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* and no required properties, call/construct signatures or index signatures
*/
function isWeakType(type: Type): boolean {
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(type as ObjectType);
return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 && resolved.indexInfos.length === 0 &&
resolved.properties.length > 0 && every(resolved.properties, p => !!(p.flags & SymbolFlags.Optional));
}
if (type.flags & TypeFlags.Substitution) {
return isWeakType((type as SubstitutionType).baseType);
}
if (type.flags & TypeFlags.Intersection) {
return every((type as IntersectionType).types, isWeakType);
return isWeakTypeWorker(type) > 0;

function isWeakTypeWorker(type: Type): -1 | 0 | 1 {
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(type as ObjectType);
return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 && resolved.indexInfos.length === 0 &&
resolved.properties.length > 0 ? (every(resolved.properties, p => !!(p.flags & SymbolFlags.Optional)) ? 1 : 0) : -1;
}
if (type.flags & TypeFlags.Substitution) {
return isWeakTypeWorker((type as SubstitutionType).baseType);
}
if (type.flags & TypeFlags.Intersection) {
let result: -1 | 0 | 1 = -1;
for (const t of (type as IntersectionType).types) {
const v = isWeakTypeWorker(t);
if (!v) {
return 0;
}
result = result > v ? result : v;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind the fix is that an empty object type should not be considered to be weak on its own. That's comes from the pre-existing requirement for weak types:

resolved.properties.length > 0 && every(resolved.properties, p => !!(p.flags & SymbolFlags.Optional))

However, when it's intersected with a weak type that should not make it, out of a sudden, non-weak. The new logic still maintains the invariant that an intersection of many empty interfaces is not considered to be weak (it should obey the same rules as a single empty interface).

All of that doesn't change the definition put in the existing code comment:

    /**
     * A type is 'weak' if it is an object type with at least one optional property
     * and no required properties, call/construct signatures or index signatures
     */

Actually, the old behavior wasn't enforcing those rules for intersections precisely enough. The proposed adjustment is closer to checking the intersection as a single type with a flat list of properties instead of checking separate types and combining those results "blindly". I think it's more accurate to do it this way because for most purposes an intersection shouldn't be distinguishable from the same type with a "flattened" list of properties.

}
return result;
}
return 0;
}
return false;
}

function hasCommonProperties(source: Type, target: Type, isComparingJsxAttributes: boolean) {
Expand Down
50 changes: 50 additions & 0 deletions tests/baselines/reference/weakTypeIntersections1.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
weakTypeIntersections1.ts(11,7): error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo> & ThisType<{ x: string; }>'.
weakTypeIntersections1.ts(12,7): error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo>'.
weakTypeIntersections1.ts(13,7): error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo> & EmptyInterface'.
weakTypeIntersections1.ts(15,7): error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo> & Partial<Bar>'.
weakTypeIntersections1.ts(23,1): error TS2769: No overload matches this call.
Overload 1 of 2, '(arg: () => Foo & ThisType<Foo>): void', gave the following error.
Type 'number' is not assignable to type 'string'.
Overload 2 of 2, '(arg: Partial<Foo> & ThisType<Foo>): void', gave the following error.
Type '() => { a: number; }' has no properties in common with type 'Partial<Foo> & ThisType<Foo>'.


==== weakTypeIntersections1.ts (5 errors) ====
interface EmptyInterface {}
interface EmptyInterface2 {}

interface Foo {
a: string;
}
interface Bar {
b: string;
}

const m1: Partial<Foo> & ThisType<{ x: string }> = () => null; // error
~~
!!! error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo> & ThisType<{ x: string; }>'.
const m2: Partial<Foo> = () => null; // error
~~
!!! error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo>'.
const m3: Partial<Foo> & EmptyInterface = () => null; // error
~~
!!! error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo> & EmptyInterface'.
const m4: EmptyInterface & EmptyInterface2 = () => null; // ok
const m5: Partial<Foo> & Partial<Bar> = () => null; // error
~~
!!! error TS2559: Type '() => null' has no properties in common with type 'Partial<Foo> & Partial<Bar>'.

// https://github.com/microsoft/TypeScript/issues/56995
declare function fun0(arg: () => Foo & ThisType<Foo>): void;
declare function fun0(arg: Partial<Foo> & ThisType<Foo>): void;

fun0({ a: "1" }); // ok
fun0(() => ({ a: "1" })); // ok
fun0(() => ({ a: 1 })); // error
~~~~
!!! error TS2769: No overload matches this call.
!!! error TS2769: Overload 1 of 2, '(arg: () => Foo & ThisType<Foo>): void', gave the following error.
!!! error TS2769: Type 'number' is not assignable to type 'string'.
!!! error TS2769: Overload 2 of 2, '(arg: Partial<Foo> & ThisType<Foo>): void', gave the following error.
!!! error TS2769: Type '() => { a: number; }' has no properties in common with type 'Partial<Foo> & ThisType<Foo>'.
!!! related TS6500 weakTypeIntersections1.ts:5:3: The expected type comes from property 'a' which is declared here on type 'Foo & ThisType<Foo>'
80 changes: 80 additions & 0 deletions tests/baselines/reference/weakTypeIntersections1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//// [tests/cases/compiler/weakTypeIntersections1.ts] ////

=== weakTypeIntersections1.ts ===
interface EmptyInterface {}
>EmptyInterface : Symbol(EmptyInterface, Decl(weakTypeIntersections1.ts, 0, 0))

interface EmptyInterface2 {}
>EmptyInterface2 : Symbol(EmptyInterface2, Decl(weakTypeIntersections1.ts, 0, 27))

interface Foo {
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))

a: string;
>a : Symbol(Foo.a, Decl(weakTypeIntersections1.ts, 3, 15))
}
interface Bar {
>Bar : Symbol(Bar, Decl(weakTypeIntersections1.ts, 5, 1))

b: string;
>b : Symbol(Bar.b, Decl(weakTypeIntersections1.ts, 6, 15))
}

const m1: Partial<Foo> & ThisType<{ x: string }> = () => null; // error
>m1 : Symbol(m1, Decl(weakTypeIntersections1.ts, 10, 5))
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))
>ThisType : Symbol(ThisType, Decl(lib.es5.d.ts, --, --))
>x : Symbol(x, Decl(weakTypeIntersections1.ts, 10, 35))

const m2: Partial<Foo> = () => null; // error
>m2 : Symbol(m2, Decl(weakTypeIntersections1.ts, 11, 5))
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))

const m3: Partial<Foo> & EmptyInterface = () => null; // error
>m3 : Symbol(m3, Decl(weakTypeIntersections1.ts, 12, 5))
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))
>EmptyInterface : Symbol(EmptyInterface, Decl(weakTypeIntersections1.ts, 0, 0))

const m4: EmptyInterface & EmptyInterface2 = () => null; // ok
>m4 : Symbol(m4, Decl(weakTypeIntersections1.ts, 13, 5))
>EmptyInterface : Symbol(EmptyInterface, Decl(weakTypeIntersections1.ts, 0, 0))
>EmptyInterface2 : Symbol(EmptyInterface2, Decl(weakTypeIntersections1.ts, 0, 27))

const m5: Partial<Foo> & Partial<Bar> = () => null; // error
>m5 : Symbol(m5, Decl(weakTypeIntersections1.ts, 14, 5))
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
>Bar : Symbol(Bar, Decl(weakTypeIntersections1.ts, 5, 1))

// https://github.com/microsoft/TypeScript/issues/56995
declare function fun0(arg: () => Foo & ThisType<Foo>): void;
>fun0 : Symbol(fun0, Decl(weakTypeIntersections1.ts, 14, 51), Decl(weakTypeIntersections1.ts, 17, 60))
>arg : Symbol(arg, Decl(weakTypeIntersections1.ts, 17, 22))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))
>ThisType : Symbol(ThisType, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))

declare function fun0(arg: Partial<Foo> & ThisType<Foo>): void;
>fun0 : Symbol(fun0, Decl(weakTypeIntersections1.ts, 14, 51), Decl(weakTypeIntersections1.ts, 17, 60))
>arg : Symbol(arg, Decl(weakTypeIntersections1.ts, 18, 22))
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))
>ThisType : Symbol(ThisType, Decl(lib.es5.d.ts, --, --))
>Foo : Symbol(Foo, Decl(weakTypeIntersections1.ts, 1, 28))

fun0({ a: "1" }); // ok
>fun0 : Symbol(fun0, Decl(weakTypeIntersections1.ts, 14, 51), Decl(weakTypeIntersections1.ts, 17, 60))
>a : Symbol(a, Decl(weakTypeIntersections1.ts, 20, 6))

fun0(() => ({ a: "1" })); // ok
>fun0 : Symbol(fun0, Decl(weakTypeIntersections1.ts, 14, 51), Decl(weakTypeIntersections1.ts, 17, 60))
>a : Symbol(a, Decl(weakTypeIntersections1.ts, 21, 13))

fun0(() => ({ a: 1 })); // error
>fun0 : Symbol(fun0, Decl(weakTypeIntersections1.ts, 14, 51), Decl(weakTypeIntersections1.ts, 17, 60))
>a : Symbol(a, Decl(weakTypeIntersections1.ts, 22, 13))

106 changes: 106 additions & 0 deletions tests/baselines/reference/weakTypeIntersections1.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//// [tests/cases/compiler/weakTypeIntersections1.ts] ////

=== weakTypeIntersections1.ts ===
interface EmptyInterface {}
interface EmptyInterface2 {}

interface Foo {
a: string;
>a : string
> : ^^^^^^
}
interface Bar {
b: string;
>b : string
> : ^^^^^^
}

const m1: Partial<Foo> & ThisType<{ x: string }> = () => null; // error
>m1 : Partial<Foo> & ThisType<{ x: string; }>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^
>x : string
> : ^^^^^^
>() => null : () => null
> : ^^^^^^^^^^

const m2: Partial<Foo> = () => null; // error
>m2 : Partial<Foo>
> : ^^^^^^^^^^^^
>() => null : () => null
> : ^^^^^^^^^^

const m3: Partial<Foo> & EmptyInterface = () => null; // error
>m3 : Partial<Foo> & EmptyInterface
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>() => null : () => null
> : ^^^^^^^^^^

const m4: EmptyInterface & EmptyInterface2 = () => null; // ok
>m4 : EmptyInterface & EmptyInterface2
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>() => null : () => null
> : ^^^^^^^^^^

const m5: Partial<Foo> & Partial<Bar> = () => null; // error
>m5 : Partial<Foo> & Partial<Bar>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>() => null : () => null
> : ^^^^^^^^^^

// https://github.com/microsoft/TypeScript/issues/56995
declare function fun0(arg: () => Foo & ThisType<Foo>): void;
>fun0 : { (arg: () => Foo & ThisType<Foo>): void; (arg: Partial<Foo> & ThisType<Foo>): void; }
> : ^^^ ^^ ^^^ ^^^ ^^ ^^^ ^^^
>arg : () => Foo & ThisType<Foo>
> : ^^^^^^

declare function fun0(arg: Partial<Foo> & ThisType<Foo>): void;
>fun0 : { (arg: () => Foo & ThisType<Foo>): void; (arg: Partial<Foo> & ThisType<Foo>): void; }
> : ^^^ ^^ ^^^ ^^^ ^^ ^^^ ^^^
>arg : Partial<Foo> & ThisType<Foo>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

fun0({ a: "1" }); // ok
>fun0({ a: "1" }) : void
> : ^^^^
>fun0 : { (arg: () => Foo & ThisType<Foo>): void; (arg: Partial<Foo> & ThisType<Foo>): void; }
> : ^^^ ^^ ^^^ ^^^ ^^ ^^^ ^^^
>{ a: "1" } : { a: string; }
> : ^^^^^^^^^^^^^^
>a : string
> : ^^^^^^
>"1" : "1"
> : ^^^

fun0(() => ({ a: "1" })); // ok
>fun0(() => ({ a: "1" })) : void
> : ^^^^
>fun0 : { (arg: () => Foo & ThisType<Foo>): void; (arg: Partial<Foo> & ThisType<Foo>): void; }
> : ^^^ ^^ ^^^ ^^^ ^^ ^^^ ^^^
>() => ({ a: "1" }) : () => { a: string; }
> : ^^^^^^^^^^^^^^^^^^^^
>({ a: "1" }) : { a: string; }
> : ^^^^^^^^^^^^^^
>{ a: "1" } : { a: string; }
> : ^^^^^^^^^^^^^^
>a : string
> : ^^^^^^
>"1" : "1"
> : ^^^

fun0(() => ({ a: 1 })); // error
>fun0(() => ({ a: 1 })) : void
> : ^^^^
>fun0 : { (arg: () => Foo & ThisType<Foo>): void; (arg: Partial<Foo> & ThisType<Foo>): void; }
> : ^^^ ^^ ^^^ ^^^ ^^ ^^^ ^^^
>() => ({ a: 1 }) : () => { a: number; }
> : ^^^^^^^^^^^^^^^^^^^^
>({ a: 1 }) : { a: number; }
> : ^^^^^^^^^^^^^^
>{ a: 1 } : { a: number; }
> : ^^^^^^^^^^^^^^
>a : number
> : ^^^^^^
>1 : 1
> : ^

26 changes: 26 additions & 0 deletions tests/cases/compiler/weakTypeIntersections1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// @strict: true
// @noEmit: true

interface EmptyInterface {}
interface EmptyInterface2 {}

interface Foo {
a: string;
}
interface Bar {
b: string;
}

const m1: Partial<Foo> & ThisType<{ x: string }> = () => null; // error
const m2: Partial<Foo> = () => null; // error
const m3: Partial<Foo> & EmptyInterface = () => null; // error
const m4: EmptyInterface & EmptyInterface2 = () => null; // ok
const m5: Partial<Foo> & Partial<Bar> = () => null; // error

// https://github.com/microsoft/TypeScript/issues/56995
declare function fun0(arg: () => Foo & ThisType<Foo>): void;
declare function fun0(arg: Partial<Foo> & ThisType<Foo>): void;

fun0({ a: "1" }); // ok
fun0(() => ({ a: "1" })); // ok
fun0(() => ({ a: 1 })); // error
Loading