diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 77f35376da7..0d5828a9a3e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17546,10 +17546,47 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return (deferredGlobalDisposableType ||= getGlobalType("Disposable" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType; } - function getGlobalAsyncDisposableType(reportErrors: boolean) { - return (deferredGlobalAsyncDisposableType ||= getGlobalType("AsyncDisposable" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } - + function getGlobalAsyncDisposableType(reportErrors: boolean) { + return (deferredGlobalAsyncDisposableType ||= getGlobalType("AsyncDisposable" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType; + } + + function checkTypeIsDisposable(type: Type): boolean { + if (type.flags & (TypeFlags.Null | TypeFlags.Undefined)) { + return true; // null and undefined are allowed + } + + // Handle union types + if (type.flags & TypeFlags.Union) { + return every((type as UnionType).types, checkTypeIsDisposable); + } + + const disposePropertyName = getPropertyNameForKnownSymbolName("dispose"); + const disposeProperty = getPropertyOfType(type, disposePropertyName); + + return !!disposeProperty && !(disposeProperty.flags & SymbolFlags.Optional); + } + + function checkTypeIsAsyncDisposable(type: Type): boolean { + if (type.flags & (TypeFlags.Null | TypeFlags.Undefined)) { + return true; // null and undefined are allowed + } + + // Handle union types + if (type.flags & TypeFlags.Union) { + return every((type as UnionType).types, checkTypeIsAsyncDisposable); + } + + const asyncDisposePropertyName = getPropertyNameForKnownSymbolName("asyncDispose"); + const asyncDisposeProperty = getPropertyOfType(type, asyncDisposePropertyName); + + if (asyncDisposeProperty && !(asyncDisposeProperty.flags & SymbolFlags.Optional)) { + return true; + } + + // For await using, also check for Symbol.dispose as a fallback + return checkTypeIsDisposable(type); + } + function getGlobalTypeOrUndefined(name: __String, arity = 0): ObjectType | undefined { const symbol = getGlobalSymbol(name, SymbolFlags.Type, /*diagnostic*/ undefined); return symbol && getTypeOfGlobalSymbol(symbol, arity) as GenericType; @@ -44997,21 +45034,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (!isJSObjectLiteralInitializer && node.parent.parent.kind !== SyntaxKind.ForInStatement) { const initializerType = checkExpressionCached(initializer); checkTypeAssignableToAndOptionallyElaborate(initializerType, type, node, initializer, /*headMessage*/ undefined); - const blockScopeKind = getCombinedNodeFlagsCached(node) & NodeFlags.BlockScoped; - if (blockScopeKind === NodeFlags.AwaitUsing) { - const globalAsyncDisposableType = getGlobalAsyncDisposableType(/*reportErrors*/ true); - const globalDisposableType = getGlobalDisposableType(/*reportErrors*/ true); - if (globalAsyncDisposableType !== emptyObjectType && globalDisposableType !== emptyObjectType) { - const optionalDisposableType = getUnionType([globalAsyncDisposableType, globalDisposableType, nullType, undefinedType]); - checkTypeAssignableTo(widenTypeForVariableLikeDeclaration(initializerType, node), optionalDisposableType, initializer, Diagnostics.The_initializer_of_an_await_using_declaration_must_be_either_an_object_with_a_Symbol_asyncDispose_or_Symbol_dispose_method_or_be_null_or_undefined); - } - } - else if (blockScopeKind === NodeFlags.Using) { - const globalDisposableType = getGlobalDisposableType(/*reportErrors*/ true); - if (globalDisposableType !== emptyObjectType) { - const optionalDisposableType = getUnionType([globalDisposableType, nullType, undefinedType]); - checkTypeAssignableTo(widenTypeForVariableLikeDeclaration(initializerType, node), optionalDisposableType, initializer, Diagnostics.The_initializer_of_a_using_declaration_must_be_either_an_object_with_a_Symbol_dispose_method_or_be_null_or_undefined); - } + const blockScopeKind = getCombinedNodeFlagsCached(node) & NodeFlags.BlockScoped; + if (blockScopeKind === NodeFlags.AwaitUsing) { + if (!checkTypeIsAsyncDisposable(widenTypeForVariableLikeDeclaration(initializerType, node))) { + error(initializer, Diagnostics.The_initializer_of_an_await_using_declaration_must_be_either_an_object_with_a_Symbol_asyncDispose_or_Symbol_dispose_method_or_be_null_or_undefined); + } + } + else if (blockScopeKind === NodeFlags.Using) { + if (!checkTypeIsDisposable(widenTypeForVariableLikeDeclaration(initializerType, node))) { + error(initializer, Diagnostics.The_initializer_of_a_using_declaration_must_be_either_an_object_with_a_Symbol_dispose_method_or_be_null_or_undefined); + } } } } diff --git a/tests/cases/compiler/usingDeclarationWithGlobalInterfaceModification.ts b/tests/cases/compiler/usingDeclarationWithGlobalInterfaceModification.ts new file mode 100644 index 00000000000..8b2aba8c503 --- /dev/null +++ b/tests/cases/compiler/usingDeclarationWithGlobalInterfaceModification.ts @@ -0,0 +1,31 @@ +// @target: esnext +// @lib: esnext + +// Test case that demonstrates the issue from https://github.com/microsoft/TypeScript/issues/62121 +// When an empty global Disposable interface is declared, it should NOT affect +// the checking for Symbol.dispose properties + +declare global { + interface Disposable {} +} + +// This should pass - has Symbol.dispose method +const validDisposable = { + [Symbol.dispose]() { + // disposed + } +}; + +// This should fail - no Symbol.dispose method +const invalidDisposable = { + cleanup() { + // cleanup + } +}; + +// With the fix, the checker should directly check for Symbol.dispose properties +// rather than relying on assignability to the global Disposable interface +using valid = validDisposable; // should pass +using invalid = invalidDisposable; // should error + +export {}; \ No newline at end of file