Preserve narrowing in closures for imports that resolve to `const` variables
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 35/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- go, typescript
- Lĩnh vực
- compilers
Hướng nghiên cứu
Start with the prototype commit linked in the issue and the Go checker's checkIdentifier and isConstantVariable logic. Review the compiler tests and baselines affected by the prototype, then add coverage for named, renamed, default, and re-exported imports that resolve to const bindings. Done means closure narrowing matches local const behavior while the listed exclusions and compatibility cases retain their expected diagnostics.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Acknowledgement
- I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.
Comment
Suggestion
A const keeps its narrowing inside callbacks. The same value imported from another module doesn't:
// @filename: a.ts
export const user: { name: string } | undefined =
Math.random() > 0.5 ? { name: "Ann" } : undefined;
// @filename: main.ts
import { user } from "./a";
if (user) {
user.name; // ok
setTimeout(() => user.name); // error TS18048: 'user' is possibly 'undefined'
}
const local = user;
if (local) {
setTimeout(() => local.name); // ok
}
TypeScript already keeps narrowing in closures for const variables and unassigned parameters (#10357), and for closures created after a variable's last assignment (#56908). Imports can be narrowed too (#16658), but in a nested closure that narrowing is lost, even when the import resolves to a const. This proposal extends the existing const rule to that case. It doesn't ask TypeScript to reason about when callbacks run (#9998).
const keeps its narrowing in closures because nothing can reassign it between the check and the call. The same is true of these imports: in an ES module, an import is an immutable binding[^1] to the exporter's binding, a const binding is immutable too[^2], and writing to either throws at runtime[^3].
The prototype covers named and renamed imports, default imports of a const (export default user), and re-export chains, including export *. Unchanged:
export let: the exporting module can still reassign it;ns.userafterimport * as ns: a property access, not an identifier;- default-exported expressions (
export default (expr)): they don't resolve to aconst; - imported function and class declarations.
Motivating Example
Checking an imported constant now works inside callbacks, the same way it does for a local const:
import { logger } from "./logger"; // export const logger: Logger | undefined
if (logger) items.forEach(item => logger.info(item)); // no more "possibly 'undefined'"
Use Cases
- Module-level values that may be absent: optional config, feature-flagged services (
export const analytics = enabled ? createAnalytics() : undefined), DOM lookups. They get checked once and then used in.map,setTimeoutor an event handler. - Removing workarounds: today people copy the import into a local
constor add!just to get past the checker. The copy is boilerplate, and!also hides a realundefinedlater. - Generated code. vue-tsc turns templates into TypeScript, where event handlers are arrow functions inside narrowed blocks, so imported constants started reporting errors there (vuejs/language-tools#6237). The proposed fix works around it in the generated code by unwrapping the reads inside closures (vuejs/language-tools#6238). For imported constants, TypeScript could keep the narrowing itself.
Prototype
The prototype was written with the help of Claude Opus 5.5 and reviewed with GPT-6 Astra.
I tried this in the Go checker (main at 8d9b6da99). JavaScript emit is unchanged. The change is one condition in checkIdentifier plus a helper (commit with the change and the test):
func (c *Checker) isConstantVariableOrAliasOfConstantVariable(symbol *ast.Symbol) bool {
// resolveAlias stops at a re-export merged with a type of the same name, so keep resolving
// until a symbol has a value meaning of its own
var seenSymbols collections.Set[*ast.Symbol]
for ast.IsNonLocalAlias(symbol, ast.SymbolFlagsValue) {
target := c.resolveAlias(symbol)
if target.Flags()&ast.SymbolFlagsAlias != 0 {
if target == symbol || seenSymbols.Has(target) {
return false
}
seenSymbols.Add(target)
}
symbol = target
}
return c.isConstantVariable(symbol)
}
The change leaves the existing compiler baselines unchanged, and benchmarks on Vuetify and vuejs/core showed no statistically significant change in check time. The same check in the JavaScript checker gives identical diagnostics on eight open-source Vue projects (Nuxt, Vuetify, VueUse and others) before and after.
Compatibility: stronger narrowing can introduce diagnostics and change inferred types in previously accepted code, including types in emitted declarations. For example, given an exported const mode: "a" | "b":
import { mode } from "./a";
if (mode === "a") {
[1].map(() => mode === "b"); // new TS2367
const modes = [1].map(() => mode); // now "a"[], previously ("a" | "b")[]
modes.push("b"); // new TS2345
}
These are the same diagnostics the example produces today with a local const mode instead of an import. The second one can break otherwise valid code; annotating the array (const modes: ("a" | "b")[]) keeps the old type.
CommonJS output is different: an import is read as a property (a_1.user), and other code could overwrite it. TypeScript already makes the same assumption in the exporting module: in a.ts, if (user) setTimeout(() => user.name) compiles to exports.user in both places and keeps its narrowing today. The change extends it to the importing side.
If this direction sounds right, I can open a PR with the change and the test.
Optional extension
Aliased conditions have the same gap for imports, even without a closure: const ok = typeof user === "object"; if (ok) user.name errors, unlike with a local const. Applying the same alias check in isConstantReference fixes it, and the existing baselines stay unchanged. I can include it in the same PR, or send it separately if you prefer.
[^1]: ECMA-262, CreateImportBinding: an import "creates a new initialized immutable indirect binding" that resolves to the exporter's binding.
[^2]: ECMA-262, InitializeEnvironment for Source Text Module Records: a module-level const is created with CreateImmutableBinding.
[^3]: ECMA-262, SetMutableBinding: writing an immutable binding throws a TypeError in strict mode code, and module code is always strict.
- Ngôn ngữ chính
- Go
- Star
- 112k
- Fork
- 15.4k
- Merge trung bình
- 2 ngày 21 giờ
- Pull request đã merge (30 ngày)
- 126
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của microsoft/TypeScript
-
Won't Fix
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
microsoft/TypeScript#65107 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Native API: formatDiagnosticsWithColorAndContext drops related information without a file locationCó thể đã có người làm @timurzvural đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
microsoft/TypeScript#65087 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
TS7 LSP: documentHighlight returns nothing when the file path contains '@'Có thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mởBug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
microsoft/TypeScript#64758 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Language server (tsc --lsp) exits with code 1 after `shutdown` and `exit`Có thể đã có người làm @rajnish-verma-22172 đã nhận 13 ngày trước. Đang mởBug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
microsoft/TypeScript#64742 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Class names `eval` and `arguments` are not reported as invalid strict mode bindingsCó thể đã có người làm @dmety đã nhận 26 ngày trước. Đang mởBug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
microsoft/TypeScript#64661 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của microsoft/TypeScript
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
hyperledger/fabric-x-orderer#1409 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
needs-area
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Azure/typespec-azure#5936 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 70/100
-
Fix mode unpins a reusable workflow call when the same repository is also used as a step actionĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
github/gh-actions-lock#261 ·
Maintainer thường phản hồi trong vòng 1 ngày