Extra check in assignment of intersections with generic constituents (#37537)

* Consolidated extra property check with intersections

* Fix comment

* Add tests

* Properly propagate intersectionState

* Route property check through recursive type tracking logic

* Accept new baselines

* Skip check when apparent type of source is never

* Accept new baselines

* Only check when apparent type of source is a structured type
This commit is contained in:
Anders Hejlsberg
2020-04-06 13:36:20 -07:00
committed by GitHub
parent 5a4024dd9d
commit a2609b1f1b
8 changed files with 282 additions and 49 deletions

View File

@@ -195,7 +195,7 @@ namespace ts {
None = 0,
Source = 1 << 0,
Target = 1 << 1,
ExcessCheck = 1 << 2,
PropertyCheck = 1 << 2,
}
const enum MappedTypeModifiers {
@@ -15561,7 +15561,7 @@ namespace ts {
if (source.flags & TypeFlags.Union) {
result = relation === comparableRelation ?
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) :
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & IntersectionState.ExcessCheck);
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState);
}
else {
if (target.flags & TypeFlags.Union) {
@@ -15569,12 +15569,6 @@ namespace ts {
}
else if (target.flags & TypeFlags.Intersection) {
result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target);
if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) && !(intersectionState & IntersectionState.ExcessCheck)) {
// Validate against excess props using the original `source`
if (!propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.ExcessCheck)) {
return Ternary.False;
}
}
}
else if (source.flags & TypeFlags.Intersection) {
// Check to see if any constituents of the intersection are immediately related to the target.
@@ -15590,9 +15584,7 @@ namespace ts {
//
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
// breaking the intersection apart.
if (!isNonGenericObjectType(target) || !every((<IntersectionType>source).types, t => isNonGenericObjectType(t) && !(getObjectFlags(t) & ObjectFlags.NonInferrableType))) {
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false, IntersectionState.Source);
}
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false, IntersectionState.Source);
}
if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) {
if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState)) {
@@ -15624,6 +15616,23 @@ namespace ts {
}
}
}
// For certain combinations involving intersections and optional, excess, or mismatched properties we need
// an extra property check where the intersection is viewed as a single object. The following are motivating
// examples that all should be errors, but aren't without this extra property check:
//
// let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
//
// declare let wrong: { a: { y: string } };
// let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
//
// function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
// x = y; // Mismatched property in source intersection
// }
if (result && (
target.flags & TypeFlags.Intersection && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) ||
isNonGenericObjectType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((<IntersectionType>source).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) {
result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck);
}
if (!result && reportErrors) {
source = originalSource.aliasSymbol ? originalSource : source;
@@ -16000,6 +16009,9 @@ namespace ts {
}
function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
if (intersectionState & IntersectionState.PropertyCheck) {
return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
}
const flags = source.flags & target.flags;
if (relation === identityRelation && !(flags & TypeFlags.Object)) {
if (flags & TypeFlags.Index) {

View File

@@ -5,23 +5,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T'.
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T'.
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
Type 'T' is not assignable to type 'T & "text"'.
Type '"text" | "email"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T'.
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
Type 'T' is not assignable to type '"text"'.
Type '"text" | "email"' is not assignable to type '"text"'.
Type '"email"' is not assignable to type '"text"'.
Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T'.
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
Type 'T' is not assignable to type 'T & "text"'.
Type '"text" | "email"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T & "text"'.
Type '"text"' is not assignable to type 'T'.
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
Type 'T' is not assignable to type '"text"'.
Type '"text" | "email"' is not assignable to type '"text"'.
Type '"email"' is not assignable to type '"text"'.
==== tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.ts (1 errors) ====
@@ -66,23 +63,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
!!! error TS2322: Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
!!! error TS2322: Type 'T' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
!!! error TS2322: Type 'T' is not assignable to type '"text"'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type '"text"'.
!!! error TS2322: Type '"email"' is not assignable to type '"text"'.
!!! error TS2322: Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
!!! error TS2322: Type 'T' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
!!! error TS2322: Type 'T' is not assignable to type '"text"'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type '"text"'.
!!! error TS2322: Type '"email"' is not assignable to type '"text"'.
}
const newTextChannel = makeNewChannel('text');

View File

@@ -0,0 +1,46 @@
tests/cases/compiler/intersectionPropertyCheck.ts(1,68): error TS2322: Type '{ x: string; y: number; }' is not assignable to type '{ x: string; }'.
Object literal may only specify known properties, and 'y' does not exist in type '{ x: string; }'.
tests/cases/compiler/intersectionPropertyCheck.ts(4,5): error TS2322: Type '{ a: { y: string; }; }' is not assignable to type '{ a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; }'.
Types of property 'a' are incompatible.
Type '{ y: string; }' has no properties in common with type '{ x?: number | undefined; }'.
tests/cases/compiler/intersectionPropertyCheck.ts(7,3): error TS2322: Type 'T & { a: boolean; }' is not assignable to type '{ a?: string | undefined; }'.
Types of property 'a' are incompatible.
Type 'boolean' is not assignable to type 'string | undefined'.
tests/cases/compiler/intersectionPropertyCheck.ts(17,22): error TS2322: Type 'true' is not assignable to type 'string[] | undefined'.
==== tests/cases/compiler/intersectionPropertyCheck.ts (4 errors) ====
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
~~~~
!!! error TS2322: Type '{ x: string; y: number; }' is not assignable to type '{ x: string; }'.
!!! error TS2322: Object literal may only specify known properties, and 'y' does not exist in type '{ x: string; }'.
!!! related TS6500 tests/cases/compiler/intersectionPropertyCheck.ts:1:12: The expected type comes from property 'a' which is declared here on type '{ a: { x: string; }; } & { c: number; }'
declare let wrong: { a: { y: string } };
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
~~~~
!!! error TS2322: Type '{ a: { y: string; }; }' is not assignable to type '{ a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; }'.
!!! error TS2322: Types of property 'a' are incompatible.
!!! error TS2322: Type '{ y: string; }' has no properties in common with type '{ x?: number | undefined; }'.
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
x = y; // Mismatched property in source intersection
~
!!! error TS2322: Type 'T & { a: boolean; }' is not assignable to type '{ a?: string | undefined; }'.
!!! error TS2322: Types of property 'a' are incompatible.
!!! error TS2322: Type 'boolean' is not assignable to type 'string | undefined'.
}
// Repro from #36637
interface Test {
readonly hi?: string[]
}
function test<T extends object>(value: T): Test {
return { ...value, hi: true }
~~
!!! error TS2322: Type 'true' is not assignable to type 'string[] | undefined'.
!!! related TS6500 tests/cases/compiler/intersectionPropertyCheck.ts:13:12: The expected type comes from property 'hi' which is declared here on type 'Test'
}

View File

@@ -0,0 +1,42 @@
//// [intersectionPropertyCheck.ts]
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
declare let wrong: { a: { y: string } };
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
x = y; // Mismatched property in source intersection
}
// Repro from #36637
interface Test {
readonly hi?: string[]
}
function test<T extends object>(value: T): Test {
return { ...value, hi: true }
}
//// [intersectionPropertyCheck.js]
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var obj = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
var weak = wrong; // Nested weak object type
function foo(x, y) {
x = y; // Mismatched property in source intersection
}
function test(value) {
return __assign(__assign({}, value), { hi: true });
}

View File

@@ -0,0 +1,58 @@
=== tests/cases/compiler/intersectionPropertyCheck.ts ===
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
>obj : Symbol(obj, Decl(intersectionPropertyCheck.ts, 0, 3))
>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 0, 10))
>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 0, 15))
>c : Symbol(c, Decl(intersectionPropertyCheck.ts, 0, 33))
>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 0, 49))
>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 0, 54))
>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 0, 66))
>c : Symbol(c, Decl(intersectionPropertyCheck.ts, 0, 74))
declare let wrong: { a: { y: string } };
>wrong : Symbol(wrong, Decl(intersectionPropertyCheck.ts, 2, 11))
>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 2, 20))
>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 2, 25))
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
>weak : Symbol(weak, Decl(intersectionPropertyCheck.ts, 3, 3))
>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 3, 11))
>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 3, 17))
>c : Symbol(c, Decl(intersectionPropertyCheck.ts, 3, 36))
>wrong : Symbol(wrong, Decl(intersectionPropertyCheck.ts, 2, 11))
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
>foo : Symbol(foo, Decl(intersectionPropertyCheck.ts, 3, 58))
>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 5, 13))
>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 5, 31))
>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 5, 35))
>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 5, 49))
>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 5, 13))
>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 5, 58))
x = y; // Mismatched property in source intersection
>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 5, 31))
>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 5, 49))
}
// Repro from #36637
interface Test {
>Test : Symbol(Test, Decl(intersectionPropertyCheck.ts, 7, 1))
readonly hi?: string[]
>hi : Symbol(Test.hi, Decl(intersectionPropertyCheck.ts, 11, 16))
}
function test<T extends object>(value: T): Test {
>test : Symbol(test, Decl(intersectionPropertyCheck.ts, 13, 1))
>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 15, 14))
>value : Symbol(value, Decl(intersectionPropertyCheck.ts, 15, 32))
>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 15, 14))
>Test : Symbol(Test, Decl(intersectionPropertyCheck.ts, 7, 1))
return { ...value, hi: true }
>value : Symbol(value, Decl(intersectionPropertyCheck.ts, 15, 32))
>hi : Symbol(hi, Decl(intersectionPropertyCheck.ts, 16, 20))
}

View File

@@ -0,0 +1,59 @@
=== tests/cases/compiler/intersectionPropertyCheck.ts ===
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
>obj : { a: { x: string;}; } & { c: number; }
>a : { x: string; }
>x : string
>c : number
>{ a: { x: 'hello', y: 2 }, c: 5 } : { a: { x: string; y: number; }; c: number; }
>a : { x: string; y: number; }
>{ x: 'hello', y: 2 } : { x: string; y: number; }
>x : string
>'hello' : "hello"
>y : number
>2 : 2
>c : number
>5 : 5
declare let wrong: { a: { y: string } };
>wrong : { a: { y: string;}; }
>a : { y: string; }
>y : string
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
>weak : { a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; }
>a : { x?: number | undefined; } | undefined
>x : number | undefined
>c : string | undefined
>wrong : { a: { y: string; }; }
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
>foo : <T extends object>(x: { a?: string;}, y: T & { a: boolean;}) => void
>x : { a?: string | undefined; }
>a : string | undefined
>y : T & { a: boolean; }
>a : boolean
x = y; // Mismatched property in source intersection
>x = y : T & { a: boolean; }
>x : { a?: string | undefined; }
>y : T & { a: boolean; }
}
// Repro from #36637
interface Test {
readonly hi?: string[]
>hi : string[] | undefined
}
function test<T extends object>(value: T): Test {
>test : <T extends object>(value: T) => Test
>value : T
return { ...value, hi: true }
>{ ...value, hi: true } : T & { hi: boolean; }
>value : T
>hi : boolean
>true : true
}

View File

@@ -5,8 +5,9 @@ tests/cases/compiler/weakType.ts(18,13): error TS2559: Type '12' has no properti
tests/cases/compiler/weakType.ts(19,13): error TS2559: Type '"completely wrong"' has no properties in common with type 'Settings'.
tests/cases/compiler/weakType.ts(20,13): error TS2559: Type 'false' has no properties in common with type 'Settings'.
tests/cases/compiler/weakType.ts(37,18): error TS2559: Type '{ error?: number; }' has no properties in common with type 'ChangeOptions'.
tests/cases/compiler/weakType.ts(62,5): error TS2326: Types of property 'properties' are incompatible.
Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'.
tests/cases/compiler/weakType.ts(62,5): error TS2322: Type '{ properties: { wrong: string; }; }' is not assignable to type 'Weak & Spoiler'.
Types of property 'properties' are incompatible.
Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'.
==== tests/cases/compiler/weakType.ts (8 errors) ====
@@ -90,7 +91,8 @@ tests/cases/compiler/weakType.ts(62,5): error TS2326: Types of property 'propert
}
let weak: Weak & Spoiler = propertiesWrong
~~~~
!!! error TS2326: Types of property 'properties' are incompatible.
!!! error TS2326: Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'.
!!! error TS2322: Type '{ properties: { wrong: string; }; }' is not assignable to type 'Weak & Spoiler'.
!!! error TS2322: Types of property 'properties' are incompatible.
!!! error TS2322: Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'.

View File

@@ -0,0 +1,20 @@
// @strict: true
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
declare let wrong: { a: { y: string } };
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
x = y; // Mismatched property in source intersection
}
// Repro from #36637
interface Test {
readonly hi?: string[]
}
function test<T extends object>(value: T): Test {
return { ...value, hi: true }
}