Skip to content

Commit 84b8483

Browse files
authored
Improvements to strictSubtypeRelation and getNarrowedType (#52282)
1 parent a8b4a3b commit 84b8483

17 files changed

+1819
-39
lines changed

src/compiler/checker.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,12 +1252,13 @@ export const enum CheckMode {
12521252

12531253
/** @internal */
12541254
export const enum SignatureCheckMode {
1255-
None = 0,
1255+
None = 0,
12561256
BivariantCallback = 1 << 0,
1257-
StrictCallback = 1 << 1,
1257+
StrictCallback = 1 << 1,
12581258
IgnoreReturnTypes = 1 << 2,
1259-
StrictArity = 1 << 3,
1260-
Callback = BivariantCallback | StrictCallback,
1259+
StrictArity = 1 << 3,
1260+
StrictTopSignature = 1 << 4,
1261+
Callback = BivariantCallback | StrictCallback,
12611262
}
12621263

12631264
const enum IntersectionState {
@@ -19582,12 +19583,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1958219583
type ErrorReporter = (message: DiagnosticMessage, arg0?: string, arg1?: string) => void;
1958319584

1958419585
/**
19585-
* Returns true if `s` is `(...args: any[]) => any` or `(this: any, ...args: any[]) => any`
19586+
* Returns true if `s` is `(...args: A) => R` where `A` is `any`, `any[]`, `never`, or `never[]`, and `R` is `any` or `unknown`.
1958619587
*/
19587-
function isAnySignature(s: Signature) {
19588-
return !s.typeParameters && (!s.thisParameter || isTypeAny(getTypeOfParameter(s.thisParameter))) && s.parameters.length === 1 &&
19589-
signatureHasRestParameter(s) && (getTypeOfParameter(s.parameters[0]) === anyArrayType || isTypeAny(getTypeOfParameter(s.parameters[0]))) &&
19590-
isTypeAny(getReturnTypeOfSignature(s));
19588+
function isTopSignature(s: Signature) {
19589+
if (!s.typeParameters && (!s.thisParameter || isTypeAny(getTypeOfParameter(s.thisParameter))) && s.parameters.length === 1 && signatureHasRestParameter(s)) {
19590+
const paramType = getTypeOfParameter(s.parameters[0]);
19591+
const restType = isArrayType(paramType) ? getTypeArguments(paramType)[0] : paramType;
19592+
return !!(restType.flags & (TypeFlags.Any | TypeFlags.Never) && getReturnTypeOfSignature(s).flags & TypeFlags.AnyOrUnknown);
19593+
}
19594+
return false;
1959119595
}
1959219596

1959319597
/**
@@ -19606,9 +19610,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1960619610
return Ternary.True;
1960719611
}
1960819612

19609-
if (isAnySignature(target)) {
19613+
if (!(checkMode & SignatureCheckMode.StrictTopSignature && isTopSignature(source)) && isTopSignature(target)) {
1961019614
return Ternary.True;
1961119615
}
19616+
if (checkMode & SignatureCheckMode.StrictTopSignature && isTopSignature(source) && !isTopSignature(target)) {
19617+
return Ternary.False;
19618+
}
1961219619

1961319620
const targetCount = getParameterCount(target);
1961419621
const sourceHasMoreParameters = !hasEffectiveRestParameter(target) &&
@@ -19864,7 +19871,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1986419871
function isSimpleTypeRelatedTo(source: Type, target: Type, relation: Map<string, RelationComparisonResult>, errorReporter?: ErrorReporter) {
1986519872
const s = source.flags;
1986619873
const t = target.flags;
19867-
if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType) return true;
19874+
if (t & TypeFlags.Any || s & TypeFlags.Never || source === wildcardType) return true;
19875+
if (t & TypeFlags.Unknown && !(relation === strictSubtypeRelation && s & TypeFlags.Any)) return true;
1986819876
if (t & TypeFlags.Never) return false;
1986919877
if (s & TypeFlags.StringLike && t & TypeFlags.String) return true;
1987019878
if (s & TypeFlags.StringLiteral && s & TypeFlags.EnumLiteral &&
@@ -21486,8 +21494,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2148621494
return Ternary.False;
2148721495
}
2148821496
}
21489-
// Consider a fresh empty object literal type "closed" under the subtype relationship - this way `{} <- {[idx: string]: any} <- fresh({})`
21490-
// and not `{} <- fresh({}) <- {[idx: string]: any}`
21497+
// A fresh empty object type is never a subtype of a non-empty object type. This ensures fresh({}) <: { [x: string]: xxx }
21498+
// but not vice-versa. Without this rule, those types would be mutual subtypes.
2149121499
else if ((relation === subtypeRelation || relation === strictSubtypeRelation) && isEmptyObjectType(target) && getObjectFlags(target) & ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) {
2149221500
return Ternary.False;
2149321501
}
@@ -22142,8 +22150,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2214222150
* See signatureAssignableTo, compareSignaturesIdentical
2214322151
*/
2214422152
function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean, intersectionState: IntersectionState, incompatibleReporter: (source: Type, target: Type) => void): Ternary {
22153+
const checkMode = relation === subtypeRelation ? SignatureCheckMode.StrictTopSignature :
22154+
relation === strictSubtypeRelation ? SignatureCheckMode.StrictTopSignature | SignatureCheckMode.StrictArity :
22155+
SignatureCheckMode.None;
2214522156
return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target,
22146-
relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, reportUnreliableMapper);
22157+
checkMode, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, reportUnreliableMapper);
2214722158
function isRelatedToWorker(source: Type, target: Type, reportErrors?: boolean) {
2214822159
return isRelatedTo(source, target, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState);
2214922160
}
@@ -22239,8 +22250,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2223922250
if (sourceInfo) {
2224022251
return indexInfoRelatedTo(sourceInfo, targetInfo, reportErrors, intersectionState);
2224122252
}
22242-
if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) {
22243-
// Intersection constituents are never considered to have an inferred index signature
22253+
// Intersection constituents are never considered to have an inferred index signature. Also, in the strict subtype relation,
22254+
// only fresh object literals are considered to have inferred index signatures. This ensures { [x: string]: xxx } <: {} but
22255+
// not vice-versa. Without this rule, those types would be mutual strict subtypes.
22256+
if (!(intersectionState & IntersectionState.Source) && (relation !== strictSubtypeRelation || getObjectFlags(source) & ObjectFlags.FreshLiteral) && isObjectTypeWithInferableIndex(source)) {
2224422257
return membersRelatedToIndexInfo(source, targetInfo, reportErrors, intersectionState);
2224522258
}
2224622259
if (reportErrors) {
@@ -27089,21 +27102,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2708927102
return emptyObjectType;
2709027103
}
2709127104

27092-
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
27105+
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean): Type {
2709327106
const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0)}` : undefined;
2709427107
return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived));
2709527108
}
2709627109

2709727110
function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
27098-
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
2709927111
if (!assumeTrue) {
27100-
return filterType(type, t => !isRelated(t, candidate));
27112+
if (checkDerived) {
27113+
return filterType(type, t => !isTypeDerivedFrom(t, candidate));
27114+
}
27115+
const trueType = getNarrowedType(type, candidate, /*assumeTrue*/ true, /*checkDerived*/ false);
27116+
return filterType(type, t => !isTypeSubsetOf(t, trueType));
2710127117
}
2710227118
if (type.flags & TypeFlags.AnyOrUnknown) {
2710327119
return candidate;
2710427120
}
2710527121
// We first attempt to filter the current type, narrowing constituents as appropriate and removing
2710627122
// constituents that are unrelated to the candidate.
27123+
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
2710727124
const keyPropertyName = type.flags & TypeFlags.Union ? getKeyPropertyName(type as UnionType) : undefined;
2710827125
const narrowedType = mapType(candidate, c => {
2710927126
// If a discriminant property is available, use that to reduce the type.
@@ -27115,7 +27132,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2711527132
// prototype object types.
2711627133
const directlyRelated = mapType(matching || type, checkDerived ?
2711727134
t => isTypeDerivedFrom(t, c) ? t : isTypeDerivedFrom(c, t) ? c : neverType :
27118-
t => isTypeSubtypeOf(c, t) ? c : isTypeSubtypeOf(t, c) ? t : neverType);
27135+
t => isTypeSubtypeOf(c, t) && !isTypeIdenticalTo(c, t) ? c : isTypeSubtypeOf(t, c) ? t : neverType);
2711927136
// If no constituents are directly related, create intersections for any generic constituents that
2712027137
// are related by constraint.
2712127138
return directlyRelated.flags & TypeFlags.Never ?
@@ -36529,7 +36546,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3652936546
}
3653036547
else {
3653136548
checkAssignmentOperator(rightType);
36532-
return getRegularTypeOfObjectLiteral(rightType);
36549+
return rightType;
3653336550
}
3653436551
case SyntaxKind.CommaToken:
3653536552
if (!compilerOptions.allowUnreachableCode && isSideEffectFree(left) && !isIndirectCall(left.parent as BinaryExpression)) {

tests/baselines/reference/controlFlowBinaryOrExpression.symbols

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,19 @@ if (isNodeList(sourceObj)) {
6464
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))
6565

6666
sourceObj.length;
67-
>sourceObj.length : Symbol(NodeList.length, Decl(controlFlowBinaryOrExpression.ts, 10, 27))
67+
>sourceObj.length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
6868
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))
69-
>length : Symbol(NodeList.length, Decl(controlFlowBinaryOrExpression.ts, 10, 27))
69+
>length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
7070
}
7171

7272
if (isHTMLCollection(sourceObj)) {
7373
>isHTMLCollection : Symbol(isHTMLCollection, Decl(controlFlowBinaryOrExpression.ts, 18, 67))
7474
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))
7575

7676
sourceObj.length;
77-
>sourceObj.length : Symbol(HTMLCollection.length, Decl(controlFlowBinaryOrExpression.ts, 14, 33))
77+
>sourceObj.length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
7878
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))
79-
>length : Symbol(HTMLCollection.length, Decl(controlFlowBinaryOrExpression.ts, 14, 33))
79+
>length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
8080
}
8181

8282
if (isNodeList(sourceObj) || isHTMLCollection(sourceObj)) {

tests/baselines/reference/controlFlowBinaryOrExpression.types

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,26 @@ if (isNodeList(sourceObj)) {
6969

7070
sourceObj.length;
7171
>sourceObj.length : number
72-
>sourceObj : NodeList
72+
>sourceObj : NodeList | HTMLCollection
7373
>length : number
7474
}
7575

7676
if (isHTMLCollection(sourceObj)) {
7777
>isHTMLCollection(sourceObj) : boolean
7878
>isHTMLCollection : (sourceObj: any) => sourceObj is HTMLCollection
79-
>sourceObj : NodeList | { a: string; }
79+
>sourceObj : EventTargetLike
8080

8181
sourceObj.length;
8282
>sourceObj.length : number
83-
>sourceObj : HTMLCollection
83+
>sourceObj : NodeList | HTMLCollection
8484
>length : number
8585
}
8686

8787
if (isNodeList(sourceObj) || isHTMLCollection(sourceObj)) {
8888
>isNodeList(sourceObj) || isHTMLCollection(sourceObj) : boolean
8989
>isNodeList(sourceObj) : boolean
9090
>isNodeList : (sourceObj: any) => sourceObj is NodeList
91-
>sourceObj : HTMLCollection | { a: string; }
91+
>sourceObj : EventTargetLike
9292
>isHTMLCollection(sourceObj) : boolean
9393
>isHTMLCollection : (sourceObj: any) => sourceObj is HTMLCollection
9494
>sourceObj : { a: string; }

tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.types

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ if (isObject1(obj1)) {
2323
}
2424
// check type after conditional block
2525
obj1;
26-
>obj1 : Record<string, unknown>
26+
>obj1 : {}
2727

2828
declare const obj2: {} | undefined;
2929
>obj2 : {} | undefined
@@ -43,7 +43,7 @@ if (isObject1(obj2)) {
4343
}
4444
// check type after conditional block
4545
obj2;
46-
>obj2 : Record<string, unknown> | undefined
46+
>obj2 : {} | undefined
4747

4848
declare function isObject2(value: unknown): value is {};
4949
>isObject2 : (value: unknown) => value is {}
@@ -67,7 +67,7 @@ if (isObject2(obj3)) {
6767
}
6868
// check type after conditional block
6969
obj3;
70-
>obj3 : {}
70+
>obj3 : Record<string, unknown>
7171

7272
declare const obj4: Record<string, unknown> | undefined;
7373
>obj4 : Record<string, unknown> | undefined
@@ -87,5 +87,5 @@ if (isObject2(obj4)) {
8787
}
8888
// check type after conditional block
8989
obj4;
90-
>obj4 : {} | undefined
90+
>obj4 : Record<string, unknown> | undefined
9191

tests/baselines/reference/instanceofWithStructurallyIdenticalTypes.symbols

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ function foo2(x: C1 | C2 | C3): string {
9595
>x : Symbol(x, Decl(instanceofWithStructurallyIdenticalTypes.ts, 23, 14))
9696

9797
return x.item;
98-
>x.item : Symbol(C1.item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10))
98+
>x.item : Symbol(item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10), Decl(instanceofWithStructurallyIdenticalTypes.ts, 4, 10))
9999
>x : Symbol(x, Decl(instanceofWithStructurallyIdenticalTypes.ts, 23, 14))
100-
>item : Symbol(C1.item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10))
100+
>item : Symbol(item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10), Decl(instanceofWithStructurallyIdenticalTypes.ts, 4, 10))
101101
}
102102
else if (isC2(x)) {
103103
>isC2 : Symbol(isC2, Decl(instanceofWithStructurallyIdenticalTypes.ts, 19, 66))

tests/baselines/reference/instanceofWithStructurallyIdenticalTypes.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function foo2(x: C1 | C2 | C3): string {
8585

8686
return x.item;
8787
>x.item : string
88-
>x : C1
88+
>x : C1 | C3
8989
>item : string
9090
}
9191
else if (isC2(x)) {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//// [narrowingMutualSubtypes.ts]
2+
// Check that `any` is a strict supertype of `unknown`
3+
4+
declare const ru1: { [x: string]: unknown };
5+
declare const ra1: { [x: string]: any };
6+
7+
const a1a = [ru1, ra1]; // { [x: string]: any }[]
8+
const a1b = [ra1, ru1]; // { [x: string]: any }[]
9+
10+
declare const ra2: { [x: string]: any };
11+
declare const ru2: { [x: string]: unknown };
12+
13+
const a2a = [ru2, ra2]; // { [x: string]: any }[]
14+
const a2b = [ra2, ru2]; // { [x: string]: any }[]
15+
16+
// Check that `{}` is strict supertype of any non-empty object
17+
18+
const c3 = {};
19+
declare const r3: { [x: string]: unknown }
20+
21+
const a3a = [c3, r3]; // {}[]
22+
const a3b = [r3, c3]; // {}[]
23+
24+
declare const r4: { [x: string]: unknown }
25+
const c4 = {};
26+
27+
const a4a = [c4, r4]; // {}[]
28+
const a4b = [r4, c4]; // {}[]
29+
30+
// Check that narrowing preserves original type in false branch for non-identical mutual subtypes
31+
32+
declare function isObject1(value: unknown): value is Record<string, unknown>;
33+
34+
function gg(x: {}) {
35+
if (isObject1(x)) {
36+
x; // Record<string, unknown>
37+
}
38+
else {
39+
x; // {}
40+
}
41+
x; // {}
42+
}
43+
44+
declare function isObject2(value: unknown): value is {};
45+
46+
function gg2(x: Record<string, unknown>) {
47+
if (isObject2(x)) {
48+
x; // {}
49+
}
50+
else {
51+
x; // Record<string, unknown>
52+
}
53+
x; // Record<string, unknown>
54+
}
55+
56+
// Repro from #50916
57+
58+
type Identity<T> = {[K in keyof T]: T[K]};
59+
60+
type Self<T> = T extends unknown ? Identity<T> : never;
61+
62+
function is<T>(value: T): value is Self<T> {
63+
return true;
64+
}
65+
66+
type Union = {a: number} | {b: number} | {c: number};
67+
68+
function example(x: Union) {
69+
if (is(x)) {}
70+
if (is(x)) {}
71+
if (is(x)) {}
72+
if (is(x)) {}
73+
if (is(x)) {}
74+
if (is(x)) {}
75+
if (is(x)) {}
76+
if (is(x)) {}
77+
x; // Union
78+
}
79+
80+
81+
//// [narrowingMutualSubtypes.js]
82+
"use strict";
83+
// Check that `any` is a strict supertype of `unknown`
84+
var a1a = [ru1, ra1]; // { [x: string]: any }[]
85+
var a1b = [ra1, ru1]; // { [x: string]: any }[]
86+
var a2a = [ru2, ra2]; // { [x: string]: any }[]
87+
var a2b = [ra2, ru2]; // { [x: string]: any }[]
88+
// Check that `{}` is strict supertype of any non-empty object
89+
var c3 = {};
90+
var a3a = [c3, r3]; // {}[]
91+
var a3b = [r3, c3]; // {}[]
92+
var c4 = {};
93+
var a4a = [c4, r4]; // {}[]
94+
var a4b = [r4, c4]; // {}[]
95+
function gg(x) {
96+
if (isObject1(x)) {
97+
x; // Record<string, unknown>
98+
}
99+
else {
100+
x; // {}
101+
}
102+
x; // {}
103+
}
104+
function gg2(x) {
105+
if (isObject2(x)) {
106+
x; // {}
107+
}
108+
else {
109+
x; // Record<string, unknown>
110+
}
111+
x; // Record<string, unknown>
112+
}
113+
function is(value) {
114+
return true;
115+
}
116+
function example(x) {
117+
if (is(x)) { }
118+
if (is(x)) { }
119+
if (is(x)) { }
120+
if (is(x)) { }
121+
if (is(x)) { }
122+
if (is(x)) { }
123+
if (is(x)) { }
124+
if (is(x)) { }
125+
x; // Union
126+
}

0 commit comments

Comments
 (0)