TypeScript: Generic Indexed Access & Unknown Mismatch
Why does TypeScript allow assigning to generic B['Child'] (unknown) but fail unknown extends B['Child'] in conditionals? Explore compiler rules, GitHub issues like #46076, and workarounds like tuple wrapping for consistent generics.
In TypeScript, inconsistent interpretation of indexed access on generic types extending a base type with unknown properties
Consider this example:
type Parent = {
Child: unknown;
};
function generic<B extends Parent>() {
const child: B["Child"] = 100; // No error: B["Child"] treated as `unknown`
type Test = unknown extends B["Child"] ? "yes" : "no";
const check: Test = "yes"; // Error: B["Child"] not treated as `unknown`
}
Why is B["Child"] assignable from any value (like a number) in variable assignments, but not substitutable for unknown in conditional types? Is this expected TypeScript behavior, a bug in generic indexed access types, or is there a reliable workaround to consistently treat B["Child"] as unknown?
In TypeScript generics, indexed access like B["Child"]—where B extends Parent and Parent has Child: unknown—appears assignable from any value in variable declarations because the compiler uses a special, intentionally unsound rule comparing against unknown’s base constraint. But conditional types like unknown extends B["Child"] fail strictly, treating the access as potentially narrower due to how extends evaluates assignability in that context. This mismatch is expected behavior, not a bug, rooted in practical design tradeoffs for generic indexed access, though workarounds like tuple wrapping make it consistent.
Contents
- The Problem in Action
- Why Assignments Seem Permissive
- Why Conditionals Fail
- The Compiler Rule Behind It
- Reliable Workarounds
- Best Practices for TypeScript Generics
- Sources
- Conclusion
The Problem in Action
Ever hit a wall where TypeScript lets you assign a number to a generic indexed type that should be unknown, but then chokes on a simple conditional check? That’s exactly what’s happening here.
Take your example:
type Parent = {
Child: unknown;
};
function generic<B extends Parent>() {
const child: B["Child"] = 100; // ✅ No error—acts like unknown
type Test = unknown extends B["Child"] ? "yes" : "no";
const check: Test = "yes"; // ❌ Error: Type '"no"' is not assignable to type '"yes" | "no"'
}
The assignment flies because B["Child"] effectively widens to accept anything (since unknown does). But the conditional flips to “no,” implying unknown doesn’t extend B["Child"]. Confusing? Absolutely. And it’s not isolated—plenty of devs run into this with TypeScript generics involving indexed access and unknown.
This isn’t random. It’s how the compiler balances soundness with usability in generic contexts.
Why Assignments Seem Permissive
Assignments work because TypeScript’s type checker has a shortcut for generic indexed access. When you do const child: B["Child"] = 100, it doesn’t fully instantiate B["Child"] to inspect the concrete shape. Instead, it checks against the base constraint of the indexed type.
Here, Parent["Child"] is unknown. And unknown? Anything assigns to it. Number? String? Even null? Sure. That’s the point of unknown—the top type for safe handling.
But why the leniency with generics? Without it, every generic function touching indexed properties would explode with errors until you passed concrete args. Imagine rewriting half your libraries. No thanks.
function logChild<B extends Parent>(b: B) {
console.log(b.Child.toFixed()); // Might error without concrete B, but assignment to B["Child"] stays chill
}
Frustrating when you want strictness, but it keeps generics usable.
Why Conditionals Fail
Conditionals are pickier. The TypeScript handbook on conditional types spells it out: T extends U ? X : Y picks the true branch only if T is assignable to U.
So unknown extends B["Child"]? asks: does unknown assign to whatever B["Child"] resolves to? In a conditional, the compiler evaluates this more strictly, without the base-constraint shortcut. B["Child"] could be number | string for some B satisfying the constraint, and unknown doesn’t assign to that—it’s the supertype, not subtype.
Quick test:
type IsUnknown<T> = unknown extends T ? true : false;
type Test1 = IsUnknown<unknown>; // true
type Test2 = IsUnknown<number>; // false
type Test3 = IsUnknown<B["Child"]>; // false! (surprise)
From Stack Overflow discussions, this boils down to unknown’s semantics: everything extends unknown, but unknown only extends itself or any. Generics muddy it further.
The Compiler Rule Behind It
Dig into the source: TypeScript’s rule for indexed access assignability is knowingly unsound. Issue #46076 nails it: “A type S is related to a type T[K] if S is related to C, where C is the base constraint of T[K] for writing.”
Translation: For writing to T[K] (like your assignment), it checks if your value matches unknown’s constraint, not the full T[K]. Practical? Yes. Sound? Nope—B could declare Child as string, making 100 invalid at runtime.
Related issues pile on:
- #26796:
unknownweirdness with constraints. - #21368: Indexed access refusing obvious assignments.
- #27470:
undefined extends T[K]quirks.
The team admits tradeoffs for real-world code. Not a bug—design.
Reliable Workarounds
Want consistency? Force strict evaluation. Two battle-tested tricks:
- Tuple Wrapper (Distributive Bypass): Wrap in single-element tuples. Conditionals distribute over unions, but tuples don’t.
type Test = [unknown] extends [B["Child"]] ? "yes" : "no"; // ✅ "yes"
Why? [T] prevents distribution, evaluating the full type. From handbook infer patterns and community fixes.
- Intermediate Capture: Extract the access first.
type ChildType<B extends Parent> = B["Child"];
type Test = unknown extends ChildType<B> ? "yes" : "no"; // ✅ "yes" (now strict)
Or for functions:
function isChildUnknown<B extends Parent>(): unknown extends B["Child"] ? true : false {
return true as any; // With wrapper, this holds
}
These sidestep the shortcut. Pick based on context—tuples for quick checks, intermediates for reusability.
Best Practices for TypeScript Generics
Don’t fight the compiler—lean in.
- Prefer explicit constraints:
B extends Parent & { Child: Exclude<unknown, null | undefined> }if null-safety matters. - Use tuples/arrays for type-level equality checks.
- Test in playground: TypeScript Playground with
--strict. - For libs, overloads or mapped types avoid deep generics.
When does this bite? Rarely in plain objects, more in React props or APIs with dynamic shapes. File issues if it’s extreme, but cite #46076—they know.
Sources
- TypeScript Handbook: Conditional Types
- TypeScript Handbook: Advanced Types
- GitHub Issue #46076: Indexed Access Assignability Rule
- GitHub Issue #26796: Unknown Constraints
- GitHub Issue #21368: Indexed Access Regression
- GitHub Issue #27470: Undefined in Mapped Types
- Stack Overflow: Unknown in Conditionals
- Stack Overflow: Extends Semantics
Conclusion
TypeScript’s handling of generic indexed access like B["Child"] with unknown prioritizes usability over perfect soundness—assignments loosen via base constraints, conditionals stay strict. It’s by design, documented in core issues. Stick to tuple wrappers or type captures for reliability; they’ll keep your generics predictable without hacks. Next time you see this, you’ll know: not a bug, just TypeScript being TypeScript.