Programming

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.

1 answer 1 view

In TypeScript, inconsistent interpretation of indexed access on generic types extending a base type with unknown properties

Consider this example:

ts
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

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:

ts
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.

ts
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:

ts
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: unknown weirdness 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:

  1. Tuple Wrapper (Distributive Bypass): Wrap in single-element tuples. Conditionals distribute over unions, but tuples don’t.
ts
type Test = [unknown] extends [B["Child"]] ? "yes" : "no"; // ✅ "yes"

Why? [T] prevents distribution, evaluating the full type. From handbook infer patterns and community fixes.

  1. Intermediate Capture: Extract the access first.
ts
type ChildType<B extends Parent> = B["Child"];
type Test = unknown extends ChildType<B> ? "yes" : "no"; // ✅ "yes" (now strict)

Or for functions:

ts
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

  1. TypeScript Handbook: Conditional Types
  2. TypeScript Handbook: Advanced Types
  3. GitHub Issue #46076: Indexed Access Assignability Rule
  4. GitHub Issue #26796: Unknown Constraints
  5. GitHub Issue #21368: Indexed Access Regression
  6. GitHub Issue #27470: Undefined in Mapped Types
  7. Stack Overflow: Unknown in Conditionals
  8. 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.

Authors
Verified by moderation
Moderation
TypeScript: Generic Indexed Access & Unknown Mismatch