Skip to content

Improvements to strictSubtypeRelation and getNarrowedType #52282

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

Merged
merged 12 commits into from
Feb 14, 2023
Merged
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
59 changes: 38 additions & 21 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1251,12 +1251,13 @@ export const enum CheckMode {

/** @internal */
export const enum SignatureCheckMode {
None = 0,
None = 0,
BivariantCallback = 1 << 0,
StrictCallback = 1 << 1,
StrictCallback = 1 << 1,
IgnoreReturnTypes = 1 << 2,
StrictArity = 1 << 3,
Callback = BivariantCallback | StrictCallback,
StrictArity = 1 << 3,
StrictTopSignature = 1 << 4,
Callback = BivariantCallback | StrictCallback,
}

const enum IntersectionState {
Expand Down Expand Up @@ -19571,12 +19572,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
type ErrorReporter = (message: DiagnosticMessage, arg0?: string, arg1?: string) => void;

/**
* Returns true if `s` is `(...args: any[]) => any` or `(this: any, ...args: any[]) => any`
* Returns true if `s` is `(...args: A) => R` where `A` is `any`, `any[]`, `never`, or `never[]`, and `R` is `any` or `unknown`.
*/
function isAnySignature(s: Signature) {
return !s.typeParameters && (!s.thisParameter || isTypeAny(getTypeOfParameter(s.thisParameter))) && s.parameters.length === 1 &&
signatureHasRestParameter(s) && (getTypeOfParameter(s.parameters[0]) === anyArrayType || isTypeAny(getTypeOfParameter(s.parameters[0]))) &&
isTypeAny(getReturnTypeOfSignature(s));
function isTopSignature(s: Signature) {
if (!s.typeParameters && (!s.thisParameter || isTypeAny(getTypeOfParameter(s.thisParameter))) && s.parameters.length === 1 && signatureHasRestParameter(s)) {
const paramType = getTypeOfParameter(s.parameters[0]);
const restType = isArrayType(paramType) ? getTypeArguments(paramType)[0] : paramType;
return !!(restType.flags & (TypeFlags.Any | TypeFlags.Never) && getReturnTypeOfSignature(s).flags & TypeFlags.AnyOrUnknown);
}
return false;
}

/**
Expand All @@ -19595,9 +19599,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return Ternary.True;
}

if (isAnySignature(target)) {
if (!(checkMode & SignatureCheckMode.StrictTopSignature && isTopSignature(source)) && isTopSignature(target)) {
return Ternary.True;
}
if (checkMode & SignatureCheckMode.StrictTopSignature && isTopSignature(source) && !isTopSignature(target)) {
return Ternary.False;
}

const targetCount = getParameterCount(target);
const sourceHasMoreParameters = !hasEffectiveRestParameter(target) &&
Expand Down Expand Up @@ -19853,7 +19860,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function isSimpleTypeRelatedTo(source: Type, target: Type, relation: Map<string, RelationComparisonResult>, errorReporter?: ErrorReporter) {
const s = source.flags;
const t = target.flags;
if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType) return true;
if (t & TypeFlags.Any || s & TypeFlags.Never || source === wildcardType) return true;
if (t & TypeFlags.Unknown && !(relation === strictSubtypeRelation && s & TypeFlags.Any)) return true;
if (t & TypeFlags.Never) return false;
if (s & TypeFlags.StringLike && t & TypeFlags.String) return true;
if (s & TypeFlags.StringLiteral && s & TypeFlags.EnumLiteral &&
Expand Down Expand Up @@ -21475,8 +21483,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return Ternary.False;
}
}
// Consider a fresh empty object literal type "closed" under the subtype relationship - this way `{} <- {[idx: string]: any} <- fresh({})`
// and not `{} <- fresh({}) <- {[idx: string]: any}`
// A fresh empty object type is never a subtype of a non-empty object type. This ensures fresh({}) <: { [x: string]: xxx }
// but not vice-versa. Without this rule, those types would be mutual subtypes.
else if ((relation === subtypeRelation || relation === strictSubtypeRelation) && isEmptyObjectType(target) && getObjectFlags(target) & ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) {
return Ternary.False;
}
Expand Down Expand Up @@ -22131,8 +22139,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* See signatureAssignableTo, compareSignaturesIdentical
*/
function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean, intersectionState: IntersectionState, incompatibleReporter: (source: Type, target: Type) => void): Ternary {
const checkMode = relation === subtypeRelation ? SignatureCheckMode.StrictTopSignature :
relation === strictSubtypeRelation ? SignatureCheckMode.StrictTopSignature | SignatureCheckMode.StrictArity :
SignatureCheckMode.None;
return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target,
relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, reportUnreliableMapper);
checkMode, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, reportUnreliableMapper);
function isRelatedToWorker(source: Type, target: Type, reportErrors?: boolean) {
return isRelatedTo(source, target, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState);
}
Expand Down Expand Up @@ -22228,8 +22239,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (sourceInfo) {
return indexInfoRelatedTo(sourceInfo, targetInfo, reportErrors, intersectionState);
}
if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) {
// Intersection constituents are never considered to have an inferred index signature
// Intersection constituents are never considered to have an inferred index signature. Also, in the strict subtype relation,
// only fresh object literals are considered to have inferred index signatures. This ensures { [x: string]: xxx } <: {} but
// not vice-versa. Without this rule, those types would be mutual strict subtypes.
if (!(intersectionState & IntersectionState.Source) && (relation !== strictSubtypeRelation || getObjectFlags(source) & ObjectFlags.FreshLiteral) && isObjectTypeWithInferableIndex(source)) {
return membersRelatedToIndexInfo(source, targetInfo, reportErrors, intersectionState);
}
if (reportErrors) {
Expand Down Expand Up @@ -27069,21 +27082,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getNarrowedType(type, targetType, assumeTrue, /*checkDerived*/ true);
}

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

function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
if (!assumeTrue) {
return filterType(type, t => !isRelated(t, candidate));
if (checkDerived) {
return filterType(type, t => !isTypeDerivedFrom(t, candidate));
}
const trueType = getNarrowedType(type, candidate, /*assumeTrue*/ true, /*checkDerived*/ false);
return filterType(type, t => !isTypeSubsetOf(t, trueType));
}
if (type.flags & TypeFlags.AnyOrUnknown) {
return candidate;
}
// We first attempt to filter the current type, narrowing constituents as appropriate and removing
// constituents that are unrelated to the candidate.
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
const keyPropertyName = type.flags & TypeFlags.Union ? getKeyPropertyName(type as UnionType) : undefined;
const narrowedType = mapType(candidate, c => {
// If a discriminant property is available, use that to reduce the type.
Expand All @@ -27095,7 +27112,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// prototype object types.
const directlyRelated = mapType(matching || type, checkDerived ?
t => isTypeDerivedFrom(t, c) ? t : isTypeDerivedFrom(c, t) ? c : neverType :
t => isTypeSubtypeOf(c, t) ? c : isTypeSubtypeOf(t, c) ? t : neverType);
t => isTypeSubtypeOf(c, t) && !isTypeIdenticalTo(c, t) ? c : isTypeSubtypeOf(t, c) ? t : neverType);
// If no constituents are directly related, create intersections for any generic constituents that
// are related by constraint.
return directlyRelated.flags & TypeFlags.Never ?
Expand Down Expand Up @@ -36492,7 +36509,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
else {
checkAssignmentOperator(rightType);
return getRegularTypeOfObjectLiteral(rightType);
return rightType;
}
case SyntaxKind.CommaToken:
if (!compilerOptions.allowUnreachableCode && isSideEffectFree(left) && !isIndirectCall(left.parent as BinaryExpression)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ if (isNodeList(sourceObj)) {
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))

sourceObj.length;
>sourceObj.length : Symbol(NodeList.length, Decl(controlFlowBinaryOrExpression.ts, 10, 27))
>sourceObj.length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))
>length : Symbol(NodeList.length, Decl(controlFlowBinaryOrExpression.ts, 10, 27))
>length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
}

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

sourceObj.length;
>sourceObj.length : Symbol(HTMLCollection.length, Decl(controlFlowBinaryOrExpression.ts, 14, 33))
>sourceObj.length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
>sourceObj : Symbol(sourceObj, Decl(controlFlowBinaryOrExpression.ts, 23, 3))
>length : Symbol(HTMLCollection.length, Decl(controlFlowBinaryOrExpression.ts, 14, 33))
>length : Symbol(length, Decl(controlFlowBinaryOrExpression.ts, 10, 27), Decl(controlFlowBinaryOrExpression.ts, 14, 33))
}

if (isNodeList(sourceObj) || isHTMLCollection(sourceObj)) {
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/controlFlowBinaryOrExpression.types
Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,26 @@ if (isNodeList(sourceObj)) {

sourceObj.length;
>sourceObj.length : number
>sourceObj : NodeList
>sourceObj : NodeList | HTMLCollection
>length : number
}

if (isHTMLCollection(sourceObj)) {
>isHTMLCollection(sourceObj) : boolean
>isHTMLCollection : (sourceObj: any) => sourceObj is HTMLCollection
>sourceObj : NodeList | { a: string; }
>sourceObj : EventTargetLike

sourceObj.length;
>sourceObj.length : number
>sourceObj : HTMLCollection
>sourceObj : NodeList | HTMLCollection
>length : number
}

if (isNodeList(sourceObj) || isHTMLCollection(sourceObj)) {
>isNodeList(sourceObj) || isHTMLCollection(sourceObj) : boolean
>isNodeList(sourceObj) : boolean
>isNodeList : (sourceObj: any) => sourceObj is NodeList
>sourceObj : HTMLCollection | { a: string; }
>sourceObj : EventTargetLike
>isHTMLCollection(sourceObj) : boolean
>isHTMLCollection : (sourceObj: any) => sourceObj is HTMLCollection
>sourceObj : { a: string; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ if (isObject1(obj1)) {
}
// check type after conditional block
obj1;
>obj1 : Record<string, unknown>
>obj1 : {}

declare const obj2: {} | undefined;
>obj2 : {} | undefined
Expand All @@ -43,7 +43,7 @@ if (isObject1(obj2)) {
}
// check type after conditional block
obj2;
>obj2 : Record<string, unknown> | undefined
>obj2 : {} | undefined

declare function isObject2(value: unknown): value is {};
>isObject2 : (value: unknown) => value is {}
Expand All @@ -67,7 +67,7 @@ if (isObject2(obj3)) {
}
// check type after conditional block
obj3;
>obj3 : {}
>obj3 : Record<string, unknown>

declare const obj4: Record<string, unknown> | undefined;
>obj4 : Record<string, unknown> | undefined
Expand All @@ -87,5 +87,5 @@ if (isObject2(obj4)) {
}
// check type after conditional block
obj4;
>obj4 : {} | undefined
>obj4 : Record<string, unknown> | undefined

Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ function foo2(x: C1 | C2 | C3): string {
>x : Symbol(x, Decl(instanceofWithStructurallyIdenticalTypes.ts, 23, 14))

return x.item;
>x.item : Symbol(C1.item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10))
>x.item : Symbol(item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10), Decl(instanceofWithStructurallyIdenticalTypes.ts, 4, 10))
>x : Symbol(x, Decl(instanceofWithStructurallyIdenticalTypes.ts, 23, 14))
>item : Symbol(C1.item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10))
>item : Symbol(item, Decl(instanceofWithStructurallyIdenticalTypes.ts, 2, 10), Decl(instanceofWithStructurallyIdenticalTypes.ts, 4, 10))
}
else if (isC2(x)) {
>isC2 : Symbol(isC2, Decl(instanceofWithStructurallyIdenticalTypes.ts, 19, 66))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function foo2(x: C1 | C2 | C3): string {

return x.item;
>x.item : string
>x : C1
>x : C1 | C3
>item : string
}
else if (isC2(x)) {
Expand Down
126 changes: 126 additions & 0 deletions tests/baselines/reference/narrowingMutualSubtypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//// [narrowingMutualSubtypes.ts]
// Check that `any` is a strict supertype of `unknown`

declare const ru1: { [x: string]: unknown };
declare const ra1: { [x: string]: any };

const a1a = [ru1, ra1]; // { [x: string]: any }[]
const a1b = [ra1, ru1]; // { [x: string]: any }[]

declare const ra2: { [x: string]: any };
declare const ru2: { [x: string]: unknown };

const a2a = [ru2, ra2]; // { [x: string]: any }[]
const a2b = [ra2, ru2]; // { [x: string]: any }[]

// Check that `{}` is strict supertype of any non-empty object

const c3 = {};
declare const r3: { [x: string]: unknown }

const a3a = [c3, r3]; // {}[]
const a3b = [r3, c3]; // {}[]

declare const r4: { [x: string]: unknown }
const c4 = {};

const a4a = [c4, r4]; // {}[]
const a4b = [r4, c4]; // {}[]

// Check that narrowing preserves original type in false branch for non-identical mutual subtypes

declare function isObject1(value: unknown): value is Record<string, unknown>;

function gg(x: {}) {
if (isObject1(x)) {
x; // Record<string, unknown>
}
else {
x; // {}
}
x; // {}
}

declare function isObject2(value: unknown): value is {};

function gg2(x: Record<string, unknown>) {
if (isObject2(x)) {
x; // {}
}
else {
x; // Record<string, unknown>
}
x; // Record<string, unknown>
}

// Repro from #50916

type Identity<T> = {[K in keyof T]: T[K]};

type Self<T> = T extends unknown ? Identity<T> : never;

function is<T>(value: T): value is Self<T> {
return true;
}

type Union = {a: number} | {b: number} | {c: number};

function example(x: Union) {
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
x; // Union
}


//// [narrowingMutualSubtypes.js]
"use strict";
// Check that `any` is a strict supertype of `unknown`
var a1a = [ru1, ra1]; // { [x: string]: any }[]
var a1b = [ra1, ru1]; // { [x: string]: any }[]
var a2a = [ru2, ra2]; // { [x: string]: any }[]
var a2b = [ra2, ru2]; // { [x: string]: any }[]
// Check that `{}` is strict supertype of any non-empty object
var c3 = {};
var a3a = [c3, r3]; // {}[]
var a3b = [r3, c3]; // {}[]
var c4 = {};
var a4a = [c4, r4]; // {}[]
var a4b = [r4, c4]; // {}[]
function gg(x) {
if (isObject1(x)) {
x; // Record<string, unknown>
}
else {
x; // {}
}
x; // {}
}
function gg2(x) {
if (isObject2(x)) {
x; // {}
}
else {
x; // Record<string, unknown>
}
x; // Record<string, unknown>
}
function is(value) {
return true;
}
function example(x) {
if (is(x)) { }
if (is(x)) { }
if (is(x)) { }
if (is(x)) { }
if (is(x)) { }
if (is(x)) { }
if (is(x)) { }
if (is(x)) { }
x; // Union
}
Loading