Skip to content

Allow arbitrary catch clause type annotations #52280

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

Closed
Closed
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
29 changes: 10 additions & 19 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10219,6 +10219,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

// Use type from type annotation if one is present
const declaredType = tryGetTypeFromEffectiveTypeNode(declaration);
if (!declaredType && isCatchClauseVariableDeclarationOrBindingElement(declaration)) {
// If the catch clause is not explicitly annotated, treat it as though it were explicitly
// annotated with unknown or any, depending on useUnknownInCatchVariables.
return useUnknownInCatchVariables ? unknownType : anyType;
}
if (declaredType) {
return addOptionality(declaredType, isProperty, isOptional);
}
Expand Down Expand Up @@ -10877,18 +10882,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
members.set("exports" as __String, result);
return createAnonymousType(symbol, members, emptyArray, emptyArray, emptyArray);
}
// Handle catch clause variables
Debug.assertIsDefined(symbol.valueDeclaration);
const declaration = symbol.valueDeclaration;
if (isCatchClauseVariableDeclarationOrBindingElement(declaration)) {
const typeNode = getEffectiveTypeAnnotationNode(declaration);
if (typeNode === undefined) {
return useUnknownInCatchVariables ? unknownType : anyType;
}
const type = getTypeOfNode(typeNode);
// an errorType will make `checkTryStatement` issue an error
return isTypeAny(type) || type === unknownType ? type : errorType;
}
// Handle export default expressions
if (isSourceFile(declaration) && isJsonSourceFile(declaration)) {
if (!declaration.statements.length) {
Expand Down Expand Up @@ -27079,6 +27074,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
isFunctionLike(node) && !getImmediatelyInvokedFunctionExpression(node) ||
node.kind === SyntaxKind.ModuleBlock ||
node.kind === SyntaxKind.SourceFile ||
node.kind === SyntaxKind.CatchClause ||
node.kind === SyntaxKind.PropertyDeclaration)!;
}

Expand Down Expand Up @@ -27451,6 +27447,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const isParameter = getRootDeclaration(declaration).kind === SyntaxKind.Parameter;
const declarationContainer = getControlFlowContainer(declaration);
let flowContainer = getControlFlowContainer(node);
const isCatch = flowContainer.kind === SyntaxKind.CatchClause;
const isOuterVariable = flowContainer !== declarationContainer;
const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent);
const isModuleExports = symbol.flags & SymbolFlags.ModuleExports;
Expand All @@ -27465,7 +27462,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// We only look for uninitialized variables in strict null checking mode, and only when we can analyze
// the entire control flow graph from the variable's declaration (i.e. when the flow container and
// declaration container are the same).
const assumeInitialized = isParameter || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isSameScopedBindingElement(node, declaration) ||
const assumeInitialized = isParameter || isCatch || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isSameScopedBindingElement(node, declaration) ||
type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void)) !== 0 ||
isInTypeQuery(node) || isInAmbientOrTypeNode(node) || node.parent.kind === SyntaxKind.ExportSpecifier) ||
node.parent.kind === SyntaxKind.NonNullExpression ||
Expand Down Expand Up @@ -41230,14 +41227,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Grammar checking
if (catchClause.variableDeclaration) {
const declaration = catchClause.variableDeclaration;
const typeNode = getEffectiveTypeAnnotationNode(getRootDeclaration(declaration));
if (typeNode) {
const type = getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ false, CheckMode.Normal);
if (type && !(type.flags & TypeFlags.AnyOrUnknown)) {
grammarErrorOnFirstToken(typeNode, Diagnostics.Catch_clause_variable_type_annotation_must_be_any_or_unknown_if_specified);
}
}
else if (declaration.initializer) {
checkVariableLikeDeclaration(declaration);
if (declaration.initializer) {
grammarErrorOnFirstToken(declaration.initializer, Diagnostics.Catch_clause_variable_cannot_have_an_initializer);
}
else {
Expand Down
4 changes: 0 additions & 4 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -607,10 +607,6 @@
"category": "Error",
"code": 1195
},
"Catch clause variable type annotation must be 'any' or 'unknown' if specified.": {
"category": "Error",
"code": 1196
},
"Catch clause variable cannot have an initializer.": {
"category": "Error",
"code": 1197
Expand Down
33 changes: 18 additions & 15 deletions tests/baselines/reference/catchClauseWithTypeAnnotation.errors.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(17,36): error TS2339: Property 'foo' does not exist on type 'unknown'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(18,37): error TS2339: Property 'foo' does not exist on type 'unknown'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(19,23): error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(20,23): error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(25,23): error TS2339: Property 'toLowerCase' does not exist on type 'number'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(29,29): error TS2492: Cannot redeclare identifier 'x' in catch clause.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(30,29): error TS2403: Subsequent variable declarations must have the same type. Variable 'x' must be of type 'boolean', but here has type 'string'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(38,27): error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(39,27): error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(36,22): error TS2339: Property 'x' does not exist on type '{}'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(37,22): error TS2339: Property 'x' does not exist on type '{}'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(38,22): error TS2339: Property 'x' does not exist on type '{}'.
tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts(39,22): error TS2339: Property 'x' does not exist on type 'Error'.


==== tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts (8 errors) ====
==== tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.ts (9 errors) ====
type any1 = any;
type unknown1 = unknown;

Expand All @@ -32,16 +33,14 @@ tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.t
~~~
!!! error TS2339: Property 'foo' does not exist on type 'unknown'.
try { } catch (x: Error) { } // error in the type
~~~~~
!!! error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
try { } catch (x: object) { } // error in the type
~~~~~~
!!! error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.

try { console.log(); }
// @ts-ignore
catch (e: number) { // e should not be a `number`
console.log(e.toLowerCase());
~~~~~~~~~~~
!!! error TS2339: Property 'toLowerCase' does not exist on type 'number'.
}

// minor bug: shows that the `catch` argument is skipped when checking scope
Expand All @@ -57,13 +56,17 @@ tests/cases/conformance/statements/tryStatements/catchClauseWithTypeAnnotation.t
try { } catch ({ x }) { } // should be OK
try { } catch ({ x }: any) { x.foo; } // should be OK
try { } catch ({ x }: any1) { x.foo;} // should be OK
try { } catch ({ x }: unknown) { console.log(x); } // should be OK
try { } catch ({ x }: unknown1) { console.log(x); } // should be OK
try { } catch ({ x }: unknown) { console.log(x); } // error in the destructure
~
!!! error TS2339: Property 'x' does not exist on type '{}'.
try { } catch ({ x }: unknown1) { console.log(x); } // error in the destructure
~
!!! error TS2339: Property 'x' does not exist on type '{}'.
try { } catch ({ x }: object) { } // error in the type
~~~~~~
!!! error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
~
!!! error TS2339: Property 'x' does not exist on type '{}'.
try { } catch ({ x }: Error) { } // error in the type
~~~~~
!!! error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
~
!!! error TS2339: Property 'x' does not exist on type 'Error'.
}

8 changes: 4 additions & 4 deletions tests/baselines/reference/catchClauseWithTypeAnnotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ function fn(x: boolean) {
try { } catch ({ x }) { } // should be OK
try { } catch ({ x }: any) { x.foo; } // should be OK
try { } catch ({ x }: any1) { x.foo;} // should be OK
try { } catch ({ x }: unknown) { console.log(x); } // should be OK
try { } catch ({ x }: unknown1) { console.log(x); } // should be OK
try { } catch ({ x }: unknown) { console.log(x); } // error in the destructure
try { } catch ({ x }: unknown1) { console.log(x); } // error in the destructure
try { } catch ({ x }: object) { } // error in the type
try { } catch ({ x }: Error) { } // error in the type
}
Expand Down Expand Up @@ -124,12 +124,12 @@ function fn(x) {
catch (_d) {
var x_5 = _d.x;
console.log(x_5);
} // should be OK
} // error in the destructure
try { }
catch (_e) {
var x_6 = _e.x;
console.log(x_6);
} // should be OK
} // error in the destructure
try { }
catch (_f) {
var x_7 = _f.x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ function fn(x: boolean) {
>any1 : Symbol(any1, Decl(catchClauseWithTypeAnnotation.ts, 0, 0))
>x : Symbol(x, Decl(catchClauseWithTypeAnnotation.ts, 34, 20))

try { } catch ({ x }: unknown) { console.log(x); } // should be OK
try { } catch ({ x }: unknown) { console.log(x); } // error in the destructure
>x : Symbol(x, Decl(catchClauseWithTypeAnnotation.ts, 35, 20))
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>x : Symbol(x, Decl(catchClauseWithTypeAnnotation.ts, 35, 20))

try { } catch ({ x }: unknown1) { console.log(x); } // should be OK
try { } catch ({ x }: unknown1) { console.log(x); } // error in the destructure
>x : Symbol(x, Decl(catchClauseWithTypeAnnotation.ts, 36, 20))
>unknown1 : Symbol(unknown1, Decl(catchClauseWithTypeAnnotation.ts, 0, 16))
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/catchClauseWithTypeAnnotation.types
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ function fn(x: boolean) {
>foo : any

try { } catch (x: Error) { } // error in the type
>x : any
>x : Error

try { } catch (x: object) { } // error in the type
>x : any
>x : object

try { console.log(); }
>console.log() : void
Expand All @@ -85,7 +85,7 @@ function fn(x: boolean) {

// @ts-ignore
catch (e: number) { // e should not be a `number`
>e : any
>e : number

console.log(e.toLowerCase());
>console.log(e.toLowerCase()) : void
Expand All @@ -94,7 +94,7 @@ function fn(x: boolean) {
>log : (...data: any[]) => void
>e.toLowerCase() : any
>e.toLowerCase : any
>e : any
>e : number
>toLowerCase : any
}

Expand Down Expand Up @@ -126,15 +126,15 @@ function fn(x: boolean) {
>x : any
>foo : any

try { } catch ({ x }: unknown) { console.log(x); } // should be OK
try { } catch ({ x }: unknown) { console.log(x); } // error in the destructure
>x : any
>console.log(x) : void
>console.log : (...data: any[]) => void
>console : Console
>log : (...data: any[]) => void
>x : any

try { } catch ({ x }: unknown1) { console.log(x); } // should be OK
try { } catch ({ x }: unknown1) { console.log(x); } // error in the destructure
>x : any
>console.log(x) : void
>console.log : (...data: any[]) => void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
tests/cases/compiler/destructureCatchClause.ts(26,17): error TS2339: Property 'x' does not exist on type '{}'.
tests/cases/compiler/destructureCatchClause.ts(27,15): error TS2461: Type 'unknown' is not an array type.
tests/cases/compiler/destructureCatchClause.ts(29,17): error TS2339: Property 'a' does not exist on type '{}'.
tests/cases/compiler/destructureCatchClause.ts(30,17): error TS2339: Property 'a' does not exist on type '{}'.
tests/cases/compiler/destructureCatchClause.ts(32,15): error TS2461: Type 'unknown' is not an array type.
tests/cases/compiler/destructureCatchClause.ts(33,15): error TS2461: Type 'unknown' is not an array type.
tests/cases/compiler/destructureCatchClause.ts(35,17): error TS2339: Property 'a' does not exist on type '{}'.


==== tests/cases/compiler/destructureCatchClause.ts (7 errors) ====
// These are okay with useUnknownInCatchVariables=false, but not okay with useUnknownInCatchVariables=true.
try {} catch ({ x }) { x }
try {} catch ([ x ]) { x }

try {} catch ({ a: { x } }) { x }
try {} catch ({ a: [ x ] }) { x }

try {} catch ([{ x }]) { x }
try {} catch ([[ x ]]) { x }

try {} catch ({ a: { b: { c: { x }} }}) { x }


try {} catch ({ x }: any) { x }
try {} catch ([ x ]: any) { x }

try {} catch ({ a: { x } }: any) { x }
try {} catch ({ a: [ x ] }: any) { x }

try {} catch ([{ x }]: any) { x }
try {} catch ([[ x ]]: any) { x }

try {} catch ({ a: { b: { c: { x }} }}: any) { x }


try {} catch ({ x }: unknown) { x }
~
!!! error TS2339: Property 'x' does not exist on type '{}'.
try {} catch ([ x ]: unknown) { x }
~~~~~
!!! error TS2461: Type 'unknown' is not an array type.

try {} catch ({ a: { x } }: unknown) { x }
~
!!! error TS2339: Property 'a' does not exist on type '{}'.
try {} catch ({ a: [ x ] }: unknown) { x }
~
!!! error TS2339: Property 'a' does not exist on type '{}'.

try {} catch ([{ x }]: unknown) { x }
~~~~~~~
!!! error TS2461: Type 'unknown' is not an array type.
try {} catch ([[ x ]]: unknown) { x }
~~~~~~~
!!! error TS2461: Type 'unknown' is not an array type.

try {} catch ({ a: { b: { c: { x }} }}: unknown) { x }
~
!!! error TS2339: Property 'a' does not exist on type '{}'.

Loading