Generic argument narrowed from `T | undefined` collapses a dependent conditional return type to `never`/`undefined`
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- compilers
Hướng nghiên cứu
Start by running the supplied TypeScript reproduction in the Playground and compare the inferred types with and without the truthy narrowing. Trace the generic narrowing and conditional-type evaluation involved in the get call; done means the narrowed call preserves the value type and reports number | undefined rather than plain undefined, with a regression test covering the case.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Bug Report
- This is a bug, not a question.
TypeScript Version
Reproduces on 6.0.3 and on 7.0.2 (npx typescript@latest, checked 2026-09-22) — not a recent regression, present in both.
Search Terms
narrowing generic T & {}, distributive conditional type never, naked type parameter
narrowed loses distributivity, generic argument narrowed conditional return type never,
extends infer R generic narrowing never
Code
type PlainObject<T> = Exclude<T, string | number | boolean | null | undefined> & object;
type Keys<T> = PlainObject<T> extends infer R ? keyof R : never;
type ValueAt<T, K extends Keys<T>> =
PlainObject<T> extends infer R ? (K extends keyof R ? R[K] : never) : never;
declare function get<const T, const K extends Keys<T>>(obj: T, key: K): ValueAt<T, K>;
// Works as expected: no narrowing involved, return type resolves to the real value type.
function withoutNarrowing<T, K extends Keys<T>>(obj: T, key: K) {
return get(obj, key);
}
// Bug: same call, but obj/key were first narrowed down from `T | undefined` via a truthy check.
function withNarrowing<T, K extends Keys<T>>(obj: T | undefined, key: K | undefined) {
if (!obj || !key) return;
return get(obj, key);
}
const ok = withoutNarrowing({ a: 1 }, 'a'); // number -- correct
const bug = withNarrowing({ a: 1 }, 'a'); // undefined -- wrong, should be `number | undefined`
Playground (paste the code above manually if this link doesn't load it correctly)
(The playground snippet includes two extra lines, checkOk/checkBug, that force the compiler to print each variable's real type in an error message — remove them, or just hover over ok/bug instead.)
Expected behavior
bug's type should be number | undefined — same as ok, plus undefined from the
early return. obj/key are proven non-nullable at the get(obj, key) call site (right
after the if (!obj || !key) return; guard), so the call should behave identically to
withoutNarrowing.
Actual behavior
bug's type collapses to plain undefined — the ValueAt<T, K> branch silently
resolves to never, so the union ValueAt<T, K> | undefined simplifies away the real
value type entirely. No error is raised anywhere; the incorrect type just propagates
silently, which is the dangerous part (a consumer reading bug's type has no signal
anything went wrong).
Root cause (as far as I could narrow it down): after if (!obj || !key) return;, obj
narrows to T & {} rather than to T (documented since TS 4.8, to stay sound about
generics that could structurally include null/undefined). T & {} is an intersection
type, not the naked type parameter symbol T anymore — and Keys<T>/ValueAt<T, K>
rely on T being naked for their X extends infer R ? ... : never conditionals to stay
deferred (unevaluated) until a concrete T is substituted. Once inference for the get
call has to work with T & {} (and its constraint, unknown), the checker seems to try
to evaluate the conditional eagerly instead of deferring it, and it collapses to never.
This only reproduces with the X extends infer R ? ... : never indirection present in
both Keys<T> and ValueAt<T, K> — a version without that indirection (e.g. a plain
T[K] lookup instantiated through a similarly-shaped generic call) resolves correctly
even after the same narrowing, so the infer R step specifically seems to be what tips
the checker into eager evaluation.
Workaround found
Passing the type arguments explicitly at the call site avoids the bug entirely, since it
sidesteps inference off the narrowed (T & {}) argument type altogether:
function withNarrowingFixed<T, K extends Keys<T>>(obj: T | undefined, key: K | undefined) {
if (!obj || !key) return;
return get<T, K>(obj, key); // <- explicit type args: back to `number | undefined`
}
Related Issues
- https://github.com/microsoft/TypeScript/issues/57246
- https://github.com/microsoft/TypeScript/issues/57707
- https://github.com/microsoft/TypeScript/issues/46975
- https://github.com/microsoft/TypeScript/issues/54806
- https://github.com/microsoft/TypeScript/issues/52327
Related in that they deal with narrowing + generic conditional types, but none is an exact duplicate of the scenario above, as far as I could tell.
- Ngôn ngữ chính
- Go
- Star
- 111k
- Fork
- 14.4k
- Merge trung bình
- 2 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 126
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
-
Possible Improvement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
microsoft/TypeScript#64278 · 1 bình luận · 1 reaction ·
-
Docs
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
microsoft/TypeScript#64118 · 1 bình luận ·
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
microsoft/TypeScript#64094 ·
-
Docs
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
microsoft/TypeScript#63959 · 5 bình luận ·
-
Domain: lib.d.ts Help Wanted
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 91/100
microsoft/TypeScript#63722 · 4 bình luận · 1 reaction ·
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 65/100
-
bug group: validation priority: low
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
codecheckers/chekhov#51 ·
-
Creating worktree from an existing remote branch with a slash in it, has unexpected behaviour Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100