diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 54fac52e166e1..161a78ff86610 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16486,36 +16486,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function getUnionOrIntersectionTypePredicate(signatures: readonly Signature[], kind: TypeFlags | undefined): TypePredicate | undefined { - let first: TypePredicate | undefined; + let last: TypePredicate | undefined; const types: Type[] = []; for (const sig of signatures) { const pred = getTypePredicateOfSignature(sig); - if (!pred || pred.kind === TypePredicateKind.AssertsThis || pred.kind === TypePredicateKind.AssertsIdentifier) { - if (kind !== TypeFlags.Intersection) { - continue; - } - else { - return; // intersections demand all members be type predicates for the result to have a predicate - } - } - - if (first) { - if (!typePredicateKindsMatch(first, pred)) { - // No common type predicate. + if (pred) { + // Constituent type predicates must all have matching kinds. We don't create composite type predicates for assertions. + if (pred.kind !== TypePredicateKind.This && pred.kind !== TypePredicateKind.Identifier || last && !typePredicateKindsMatch(last, pred)) { return undefined; } + last = pred; + types.push(pred.type); } else { - first = pred; + // In composite union signatures we permit and ignore signatures with a return type `false`. + const returnType = kind !== TypeFlags.Intersection ? getReturnTypeOfSignature(sig) : undefined; + if (returnType !== falseType && returnType !== regularFalseType) { + return undefined; + } } - types.push(pred.type); } - if (!first) { - // No signatures had a type predicate. + if (!last) { return undefined; } const compositeType = getUnionOrIntersectionType(types, kind); - return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType); + return createTypePredicate(last.kind, last.parameterName, last.parameterIndex, compositeType); } function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean { diff --git a/tests/baselines/reference/typePredicatesInUnion3.errors.txt b/tests/baselines/reference/typePredicatesInUnion3.errors.txt new file mode 100644 index 0000000000000..8f30ea560097a --- /dev/null +++ b/tests/baselines/reference/typePredicatesInUnion3.errors.txt @@ -0,0 +1,69 @@ +tests/cases/compiler/typePredicatesInUnion3.ts(59,24): error TS2345: Argument of type 'number | null' is not assignable to parameter of type 'number'. + Type 'null' is not assignable to type 'number'. + + +==== tests/cases/compiler/typePredicatesInUnion3.ts (1 errors) ==== + // A union of function types is considered a type predicate if at least one constituent is a type + // predicate and the other constituents are matching type predicates or functions returning `false`. + + type P1 = (x: unknown) => x is string; + type P2 = (x: unknown) => x is number; + + type F1 = (x: unknown) => false; + type F2 = (x: unknown) => boolean; + type F3 = (x: unknown) => string; + + function f1(x: unknown, p: P1 | P2) { + if (p(x)) { + x; // string | number + } + } + + function f2(x: unknown, p: P1 | P2 | F1) { + if (p(x)) { + x; // string | number + } + } + + function f3(x: unknown, p: P1 | P2 | F2) { + if (p(x)) { + x; // unknown + } + } + + function f4(x: unknown, p: P1 | P2 | F3) { + if (p(x)) { + x; // unknown + } + } + + // Repro from #54143 + + type HasAttribute = T & { attribute: number }; + + class Type1 { + attribute: number | null = null; + predicate(): this is HasAttribute { + return true; + } + } + + class Type2 { + attribute: number | null = null; + predicate(): boolean { + return true; + } + } + + function assertType(_val: T) { + } + + declare const val: Type1 | Type2; + + if (val.predicate()) { + assertType(val.attribute); // Error + ~~~~~~~~~~~~~ +!!! error TS2345: Argument of type 'number | null' is not assignable to parameter of type 'number'. +!!! error TS2345: Type 'null' is not assignable to type 'number'. + } + \ No newline at end of file diff --git a/tests/baselines/reference/typePredicatesInUnion3.symbols b/tests/baselines/reference/typePredicatesInUnion3.symbols new file mode 100644 index 0000000000000..55a214ac270ee --- /dev/null +++ b/tests/baselines/reference/typePredicatesInUnion3.symbols @@ -0,0 +1,153 @@ +=== tests/cases/compiler/typePredicatesInUnion3.ts === +// A union of function types is considered a type predicate if at least one constituent is a type +// predicate and the other constituents are matching type predicates or functions returning `false`. + +type P1 = (x: unknown) => x is string; +>P1 : Symbol(P1, Decl(typePredicatesInUnion3.ts, 0, 0)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 3, 11)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 3, 11)) + +type P2 = (x: unknown) => x is number; +>P2 : Symbol(P2, Decl(typePredicatesInUnion3.ts, 3, 38)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 4, 11)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 4, 11)) + +type F1 = (x: unknown) => false; +>F1 : Symbol(F1, Decl(typePredicatesInUnion3.ts, 4, 38)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 6, 11)) + +type F2 = (x: unknown) => boolean; +>F2 : Symbol(F2, Decl(typePredicatesInUnion3.ts, 6, 32)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 7, 11)) + +type F3 = (x: unknown) => string; +>F3 : Symbol(F3, Decl(typePredicatesInUnion3.ts, 7, 34)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 8, 11)) + +function f1(x: unknown, p: P1 | P2) { +>f1 : Symbol(f1, Decl(typePredicatesInUnion3.ts, 8, 33)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 10, 12)) +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 10, 23)) +>P1 : Symbol(P1, Decl(typePredicatesInUnion3.ts, 0, 0)) +>P2 : Symbol(P2, Decl(typePredicatesInUnion3.ts, 3, 38)) + + if (p(x)) { +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 10, 23)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 10, 12)) + + x; // string | number +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 10, 12)) + } +} + +function f2(x: unknown, p: P1 | P2 | F1) { +>f2 : Symbol(f2, Decl(typePredicatesInUnion3.ts, 14, 1)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 16, 12)) +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 16, 23)) +>P1 : Symbol(P1, Decl(typePredicatesInUnion3.ts, 0, 0)) +>P2 : Symbol(P2, Decl(typePredicatesInUnion3.ts, 3, 38)) +>F1 : Symbol(F1, Decl(typePredicatesInUnion3.ts, 4, 38)) + + if (p(x)) { +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 16, 23)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 16, 12)) + + x; // string | number +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 16, 12)) + } +} + +function f3(x: unknown, p: P1 | P2 | F2) { +>f3 : Symbol(f3, Decl(typePredicatesInUnion3.ts, 20, 1)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 22, 12)) +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 22, 23)) +>P1 : Symbol(P1, Decl(typePredicatesInUnion3.ts, 0, 0)) +>P2 : Symbol(P2, Decl(typePredicatesInUnion3.ts, 3, 38)) +>F2 : Symbol(F2, Decl(typePredicatesInUnion3.ts, 6, 32)) + + if (p(x)) { +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 22, 23)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 22, 12)) + + x; // unknown +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 22, 12)) + } +} + +function f4(x: unknown, p: P1 | P2 | F3) { +>f4 : Symbol(f4, Decl(typePredicatesInUnion3.ts, 26, 1)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 28, 12)) +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 28, 23)) +>P1 : Symbol(P1, Decl(typePredicatesInUnion3.ts, 0, 0)) +>P2 : Symbol(P2, Decl(typePredicatesInUnion3.ts, 3, 38)) +>F3 : Symbol(F3, Decl(typePredicatesInUnion3.ts, 7, 34)) + + if (p(x)) { +>p : Symbol(p, Decl(typePredicatesInUnion3.ts, 28, 23)) +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 28, 12)) + + x; // unknown +>x : Symbol(x, Decl(typePredicatesInUnion3.ts, 28, 12)) + } +} + +// Repro from #54143 + +type HasAttribute = T & { attribute: number }; +>HasAttribute : Symbol(HasAttribute, Decl(typePredicatesInUnion3.ts, 32, 1)) +>T : Symbol(T, Decl(typePredicatesInUnion3.ts, 36, 18)) +>T : Symbol(T, Decl(typePredicatesInUnion3.ts, 36, 18)) +>attribute : Symbol(attribute, Decl(typePredicatesInUnion3.ts, 36, 28)) + +class Type1 { +>Type1 : Symbol(Type1, Decl(typePredicatesInUnion3.ts, 36, 49)) + + attribute: number | null = null; +>attribute : Symbol(Type1.attribute, Decl(typePredicatesInUnion3.ts, 38, 13)) + + predicate(): this is HasAttribute { +>predicate : Symbol(Type1.predicate, Decl(typePredicatesInUnion3.ts, 39, 36)) +>HasAttribute : Symbol(HasAttribute, Decl(typePredicatesInUnion3.ts, 32, 1)) +>Type1 : Symbol(Type1, Decl(typePredicatesInUnion3.ts, 36, 49)) + + return true; + } +} + +class Type2 { +>Type2 : Symbol(Type2, Decl(typePredicatesInUnion3.ts, 43, 1)) + + attribute: number | null = null; +>attribute : Symbol(Type2.attribute, Decl(typePredicatesInUnion3.ts, 45, 13)) + + predicate(): boolean { +>predicate : Symbol(Type2.predicate, Decl(typePredicatesInUnion3.ts, 46, 36)) + + return true; + } +} + +function assertType(_val: T) { +>assertType : Symbol(assertType, Decl(typePredicatesInUnion3.ts, 50, 1)) +>T : Symbol(T, Decl(typePredicatesInUnion3.ts, 52, 20)) +>_val : Symbol(_val, Decl(typePredicatesInUnion3.ts, 52, 23)) +>T : Symbol(T, Decl(typePredicatesInUnion3.ts, 52, 20)) +} + +declare const val: Type1 | Type2; +>val : Symbol(val, Decl(typePredicatesInUnion3.ts, 55, 13)) +>Type1 : Symbol(Type1, Decl(typePredicatesInUnion3.ts, 36, 49)) +>Type2 : Symbol(Type2, Decl(typePredicatesInUnion3.ts, 43, 1)) + +if (val.predicate()) { +>val.predicate : Symbol(predicate, Decl(typePredicatesInUnion3.ts, 39, 36), Decl(typePredicatesInUnion3.ts, 46, 36)) +>val : Symbol(val, Decl(typePredicatesInUnion3.ts, 55, 13)) +>predicate : Symbol(predicate, Decl(typePredicatesInUnion3.ts, 39, 36), Decl(typePredicatesInUnion3.ts, 46, 36)) + + assertType(val.attribute); // Error +>assertType : Symbol(assertType, Decl(typePredicatesInUnion3.ts, 50, 1)) +>val.attribute : Symbol(attribute, Decl(typePredicatesInUnion3.ts, 38, 13), Decl(typePredicatesInUnion3.ts, 45, 13)) +>val : Symbol(val, Decl(typePredicatesInUnion3.ts, 55, 13)) +>attribute : Symbol(attribute, Decl(typePredicatesInUnion3.ts, 38, 13), Decl(typePredicatesInUnion3.ts, 45, 13)) +} + diff --git a/tests/baselines/reference/typePredicatesInUnion3.types b/tests/baselines/reference/typePredicatesInUnion3.types new file mode 100644 index 0000000000000..7071e9ab4161d --- /dev/null +++ b/tests/baselines/reference/typePredicatesInUnion3.types @@ -0,0 +1,141 @@ +=== tests/cases/compiler/typePredicatesInUnion3.ts === +// A union of function types is considered a type predicate if at least one constituent is a type +// predicate and the other constituents are matching type predicates or functions returning `false`. + +type P1 = (x: unknown) => x is string; +>P1 : (x: unknown) => x is string +>x : unknown + +type P2 = (x: unknown) => x is number; +>P2 : (x: unknown) => x is number +>x : unknown + +type F1 = (x: unknown) => false; +>F1 : (x: unknown) => false +>x : unknown +>false : false + +type F2 = (x: unknown) => boolean; +>F2 : (x: unknown) => boolean +>x : unknown + +type F3 = (x: unknown) => string; +>F3 : (x: unknown) => string +>x : unknown + +function f1(x: unknown, p: P1 | P2) { +>f1 : (x: unknown, p: P1 | P2) => void +>x : unknown +>p : P1 | P2 + + if (p(x)) { +>p(x) : boolean +>p : P1 | P2 +>x : unknown + + x; // string | number +>x : string | number + } +} + +function f2(x: unknown, p: P1 | P2 | F1) { +>f2 : (x: unknown, p: P1 | P2 | F1) => void +>x : unknown +>p : P1 | P2 | F1 + + if (p(x)) { +>p(x) : boolean +>p : P1 | P2 | F1 +>x : unknown + + x; // string | number +>x : string | number + } +} + +function f3(x: unknown, p: P1 | P2 | F2) { +>f3 : (x: unknown, p: P1 | P2 | F2) => void +>x : unknown +>p : P1 | P2 | F2 + + if (p(x)) { +>p(x) : boolean +>p : P1 | P2 | F2 +>x : unknown + + x; // unknown +>x : unknown + } +} + +function f4(x: unknown, p: P1 | P2 | F3) { +>f4 : (x: unknown, p: P1 | P2 | F3) => void +>x : unknown +>p : P1 | P2 | F3 + + if (p(x)) { +>p(x) : string | boolean +>p : P1 | P2 | F3 +>x : unknown + + x; // unknown +>x : unknown + } +} + +// Repro from #54143 + +type HasAttribute = T & { attribute: number }; +>HasAttribute : HasAttribute +>attribute : number + +class Type1 { +>Type1 : Type1 + + attribute: number | null = null; +>attribute : number | null + + predicate(): this is HasAttribute { +>predicate : () => this is HasAttribute + + return true; +>true : true + } +} + +class Type2 { +>Type2 : Type2 + + attribute: number | null = null; +>attribute : number | null + + predicate(): boolean { +>predicate : () => boolean + + return true; +>true : true + } +} + +function assertType(_val: T) { +>assertType : (_val: T) => void +>_val : T +} + +declare const val: Type1 | Type2; +>val : Type1 | Type2 + +if (val.predicate()) { +>val.predicate() : boolean +>val.predicate : (() => this is HasAttribute) | (() => boolean) +>val : Type1 | Type2 +>predicate : (() => this is HasAttribute) | (() => boolean) + + assertType(val.attribute); // Error +>assertType(val.attribute) : void +>assertType : (_val: T) => void +>val.attribute : number | null +>val : Type1 | Type2 +>attribute : number | null +} + diff --git a/tests/cases/compiler/typePredicatesInUnion3.ts b/tests/cases/compiler/typePredicatesInUnion3.ts new file mode 100644 index 0000000000000..f6344cf9bf17a --- /dev/null +++ b/tests/cases/compiler/typePredicatesInUnion3.ts @@ -0,0 +1,63 @@ +// @strict: true +// @noEmit: true + +// A union of function types is considered a type predicate if at least one constituent is a type +// predicate and the other constituents are matching type predicates or functions returning `false`. + +type P1 = (x: unknown) => x is string; +type P2 = (x: unknown) => x is number; + +type F1 = (x: unknown) => false; +type F2 = (x: unknown) => boolean; +type F3 = (x: unknown) => string; + +function f1(x: unknown, p: P1 | P2) { + if (p(x)) { + x; // string | number + } +} + +function f2(x: unknown, p: P1 | P2 | F1) { + if (p(x)) { + x; // string | number + } +} + +function f3(x: unknown, p: P1 | P2 | F2) { + if (p(x)) { + x; // unknown + } +} + +function f4(x: unknown, p: P1 | P2 | F3) { + if (p(x)) { + x; // unknown + } +} + +// Repro from #54143 + +type HasAttribute = T & { attribute: number }; + +class Type1 { + attribute: number | null = null; + predicate(): this is HasAttribute { + return true; + } +} + +class Type2 { + attribute: number | null = null; + predicate(): boolean { + return true; + } +} + +function assertType(_val: T) { +} + +declare const val: Type1 | Type2; + +if (val.predicate()) { + assertType(val.attribute); // Error +}