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

Infer AssertsIdentifier type predicates #58495

Open
wants to merge 11 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
57 changes: 55 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15468,10 +15468,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
createTypePredicateFromTypePredicateNode(type, signature) :
jsdocPredicate || noTypePredicate;
}
else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType.flags & TypeFlags.Boolean) && getParameterCount(signature) > 0) {
else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType.flags & (TypeFlags.Boolean | TypeFlags.VoidLike)) && getParameterCount(signature) > 0) {
const { declaration } = signature;
signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop
signature.resolvedTypePredicate = getTypePredicateFromBody(declaration) || noTypePredicate;
if (!signature.resolvedReturnType || signature.resolvedReturnType.flags & TypeFlags.Boolean) {
signature.resolvedTypePredicate = getTypePredicateFromBody(declaration) || noTypePredicate;
}
else if (!signature.resolvedReturnType || signature.resolvedReturnType.flags & TypeFlags.VoidLike) {
signature.resolvedTypePredicate = getTypeAssertionFromBody(declaration) || noTypePredicate;
}
}
else {
signature.resolvedTypePredicate = noTypePredicate;
Expand Down Expand Up @@ -38016,6 +38021,54 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return falseSubtype.flags & TypeFlags.Never ? trueType : undefined;
}

function getTypeAssertionFromBody(func: FunctionLikeDeclaration): TypePredicate | undefined {
switch (func.kind) {
case SyntaxKind.Constructor:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.ArrowFunction:
case SyntaxKind.FunctionExpression:
case SyntaxKind.MethodDeclaration:
return undefined;
}
const functionFlags = getFunctionFlags(func);
if (functionFlags !== FunctionFlags.Normal || !func.body) return undefined;

const returnFlowNodes: FlowNode[] = [];
const bailedEarly = forEachReturnStatement(func.body, returnStatement => {
if (!returnStatement.flowNode) {
return true;
}
returnFlowNodes.push(returnStatement.flowNode);
});
if (bailedEarly) return undefined;
if (functionHasImplicitReturn(func)) {
returnFlowNodes.push(func.endFlowNode!);
}
if (!returnFlowNodes.length) return undefined;

return forEach(func.parameters, (param, i) => {
const initType = getTypeOfSymbol(param.symbol);
if (!initType || !isIdentifier(param.name) || isSymbolAssigned(param.symbol) || isRestParameter(param)) {
return;
}
const typesAtReturn: Type[] = [];
const bailedEarly = forEach(returnFlowNodes, flowNode => {
const type = getFlowTypeOfReference(param.name, initType, initType, func, flowNode);
if (type === initType) return true;
typesAtReturn.push(type);
});
if (bailedEarly) return;
// The asserted type might union back to be the same as the initType, which would not be a useful assertion.
// An assignability check covers this, but a void initType can become an undefined type through control flow analysis.
// Since void is not assignable to undefined, we patch initType to handle this, too.
const assertedType = getUnionType(typesAtReturn, UnionReduction.Subtype);
const patchedInitType = mapType(initType, t => t.flags & TypeFlags.Void ? undefinedType : t.flags & TypeFlags.Any ? unknownType : t);
if (isTypeAssignableTo(patchedInitType, assertedType)) return;
return createTypePredicate(TypePredicateKind.AssertsIdentifier, unescapeLeadingUnderscores(param.name.escapedText), i, assertedType);
});
}

/**
* TypeScript Specification 1.0 (6.3) - July 2014
* An explicitly typed function whose return type isn't the Void type,
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/assertionTypePredicates1.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ declare function assertIsArrayOfStrings(value: unknown): asserts value is string
declare function assertDefined<T>(value: T): asserts value is NonNullable<T>;
declare function f01(x: unknown): void;
declare function f02(x: string | undefined): void;
declare function f03(x: string | undefined, assert: (value: unknown) => asserts value): void;
declare function f03(x: string | undefined, assert: (value: unknown) => asserts value): asserts x is string;
declare namespace Debug {
function assert(value: unknown, message?: string): asserts value;
function assertDefined<T>(value: T): asserts value is NonNullable<T>;
Expand Down Expand Up @@ -429,7 +429,7 @@ declare class Wat {
get p2(): asserts this is string;
set p2(x: asserts this is string);
}
declare function f20(x: unknown): void;
declare function f20(x: unknown): asserts x is string;
interface Thing {
good: boolean;
isGood(): asserts this is GoodThing;
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/assertionTypePredicates1.types
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ function f02(x: string | undefined) {
}

function f03(x: string | undefined, assert: (value: unknown) => asserts value) {
>f03 : (x: string | undefined, assert: (value: unknown) => asserts value) => void
> : ^ ^^ ^^ ^^ ^^^^^^^^^
>f03 : (x: string | undefined, assert: (value: unknown) => asserts value) => asserts x is string
> : ^ ^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^
>x : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>assert : (value: unknown) => asserts value
Expand Down Expand Up @@ -918,8 +918,8 @@ declare class Wat {
}

function f20(x: unknown) {
>f20 : (x: unknown) => void
> : ^ ^^ ^^^^^^^^^
>f20 : (x: unknown) => asserts x is string
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^
>x : unknown
> : ^^^^^^^

Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/coAndContraVariantInferences2.types
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ declare function assertNode(node: Node | undefined, test: ((node: Node) => boole
> : ^^^^

function foo(node: FunctionDeclaration | CaseClause) {
>foo : (node: FunctionDeclaration | CaseClause) => void
> : ^ ^^ ^^^^^^^^^
>foo : (node: FunctionDeclaration | CaseClause) => asserts node is FunctionDeclaration
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>node : CaseClause | FunctionDeclaration
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ function Narrow<T>(value: any): asserts value is T {}
>value : any

function func(foo: any, bar: any) {
>func : (foo: any, bar: any) => void
> : ^ ^^ ^^ ^^ ^^^^^^^^^
>func : (foo: any, bar: any) => asserts foo is number
> : ^ ^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : any
>bar : any

Expand Down Expand Up @@ -36,8 +36,8 @@ function func(foo: any, bar: any) {
}

function func2(foo: any, bar: any, baz: any) {
>func2 : (foo: any, bar: any, baz: any) => void
> : ^ ^^ ^^ ^^ ^^ ^^ ^^^^^^^^^
>func2 : (foo: any, bar: any, baz: any) => asserts foo is number
> : ^ ^^ ^^ ^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : any
>bar : any
>baz : any
Expand Down
7 changes: 5 additions & 2 deletions tests/baselines/reference/dependentDestructuredVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -954,14 +954,17 @@ declare function foo({ value1, test1, test2, test3, test4, test5, test6, test7,
test8?: any;
test9?: any;
}): void;
declare function fa1(x: [true, number] | [false, string]): void;
declare function fa1(x: [true, number] | [false, string]): asserts x is [false, string];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one is a little weird (the function never returns), but it follows from the types: TS understands that for(;;) {} is an infinite loop, but not while(!!true) {}.

declare function fa2(x: {
guard: true;
value: number;
} | {
guard: false;
value: string;
}): void;
}): asserts x is {
guard: false;
value: string;
};
declare const fa3: (...args: [true, number] | [false, string]) => void;
interface ClientEvents {
warn: [message: string];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1565,8 +1565,8 @@ function foo({
// Repro from #49772

function fa1(x: [true, number] | [false, string]) {
>fa1 : (x: [true, number] | [false, string]) => void
> : ^ ^^ ^^^^^^^^^
>fa1 : (x: [true, number] | [false, string]) => asserts x is [false, string]
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>x : [true, number] | [false, string]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>true : true
Expand Down Expand Up @@ -1609,8 +1609,8 @@ function fa1(x: [true, number] | [false, string]) {
}

function fa2(x: { guard: true, value: number } | { guard: false, value: string }) {
>fa2 : (x: { guard: true; value: number; } | { guard: false; value: string; }) => void
> : ^ ^^ ^^^^^^^^^
>fa2 : (x: { guard: true; value: number; } | { guard: false; value: string; }) => asserts x is { guard: false; value: string; }
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^
>x : { guard: true; value: number; } | { guard: false; value: string; }
> : ^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^
>guard : true
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/discriminatedUnionTypes1.types
Original file line number Diff line number Diff line change
Expand Up @@ -659,8 +659,8 @@ function f7(m: Message) {
}

function f8(m: Message) {
>f8 : (m: Message) => void
> : ^ ^^ ^^^^^^^^^
>f8 : (m: Message) => asserts m is { kind: "A"; x: string; } | { kind: "B" | "C"; y: number; }
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>m : Message
> : ^^^^^^^

Expand Down
169 changes: 169 additions & 0 deletions tests/baselines/reference/inferTypePredicates.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,173 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1
if (foobarPred(foobar)) {
foobar.foo;
}

function assertIsNumber(x: unknown) {
if (typeof x !== 'number') {
throw new Error();
}
}

function assertIsSmallNumber(x: unknown) {
if (typeof x === 'number' && x < 10) {
return;
}
throw new Error();
}

function assertMultipleReturns(x: unknown) {
if (x instanceof Date) {
return;
} else if (x instanceof RegExp) {
return;
} else {
throw new Error();
}
}

function assertChained(x: number | string) {
assertIsNumber(x);
}

function assertOneParam(a: unknown, b: unknown) {
assertIsSmallNumber(b);
}

function nonAssertion(a: number | string) {
if (typeof a === 'number') {
return;
} else if (typeof a === 'string') {
return;
}
throw new Error();
}

function justAssert(x: unknown) {
throw new Error();
}

function assertMultiple(a: unknown, b: unknown) {
assertIsNumber(a);
assertIsNumber(b);
}

// should not return "asserts x is Date | undefined".
function assertOptional(x?: Date) {
if (x) {
return;
}
}

// should not return "asserts x is {} | null | undefined".
function splitUnknown(x: unknown) {
if (x === null) {
return;
} else if (x === undefined) {
return;
}
}

function assertionViaInfiniteLoop(x: string | number) {
if (typeof x === 'string') {
for (;;) {}
}
}

function booleanOrVoid(a: boolean | void) {
if (typeof a === "undefined") {
a
}
a
}

function assertTrue(x: boolean) {
if (!x) throw new Error();
}

function assertNonNullish<T>(x: T) {
if (x != null) {
return;
}
throw new Error();
}

function assertIsShortString(x: unknown) {
if (typeof x !== 'string') {
throw new Error('Expected string');
} else if (x.length > 10) {
throw new Error('Expected short string');
}
}

function assertABC(x: 'A' | 'B' | 'C' | 'D' | 'E') {
if (x === 'A') {
return; // type of x here is 'A'
} else if (x === 'B' || x === 'C') {
throw new Error();
}
// implicit return; type of x here is 'D' | E'
}

// this is not expected to be inferred as an assertion type predicate
// due to https://github.com/microsoft/TypeScript/issues/34523
const assertNumberArrow = (base: string | number) => {
if (typeof base !== 'number') {
throw new Error();
}
};

assertNumberArrow('hello'); // should ok

class Test {
// Methods are not inferred as assertion type predicates becasue you
// can easily run into TS2776 (https://github.com/microsoft/TypeScript/pull/33622).
assert(value: unknown) {
if (typeof value === 'number') {
return;
}
throw new Error();
}
}

function fTest(x: unknown) {
const t1 = new Test();
t1.assert(typeof x === "string"); // should ok

const t2: Test = new Test();
t2.assert(typeof x === "string"); // should ok
}

interface Named {
name: string;
}

declare function assertName(x: any): asserts x is Named;
declare function isNamed(x: any): x is Named;

function inferFromTypePred(x: unknown) {
if (!isNamed(x)) {
throw new Error();
}
}

function inferFromTypePredAny(x: any) {
if (!isNamed(x)) {
throw new Error();
}
}

// should return void, not "asserts pattern is string"
const assertWithFuncExpr = function (pattern: unknown) {
if (typeof pattern !== 'string') {
throw new TypeError('invalid pattern')
}

if (pattern.length > 1024) {
throw new TypeError('pattern is too long')
}
}

function useAssertWithFuncExpr(pattern: string) {
assertWithFuncExpr(pattern);
}

Loading