Skip to content

Typescript incorrectly resolves discriminated unions of indexed types #37052

Closed

Description

@bibachhetri

TypeScript Version: 3.8-Beta

Search Terms: discriminated unions indexed types

Expected behavior:
b1 should be assignable to a1

Actual behavior:
The following error Type '"B"' is not assignable to type '"A"'

Code

type TinyModel = {
    "A": "A" | "B";
    "B": "B";
}

type TinyId<T extends keyof TinyModel> = {
    type: TinyModel[T];
}

let a1: TinyId<"A">;
let b1: TinyId<"B">;

// Should be semantically equivalent to a1 & b1
let a2: { type: "A" | "B" };
let b2: { type: "B" };

function fn1() { a1 = b1; } // this should work, but doesn't
function fn3() { a2 = b2; }
Output
"use strict";
let a1;
let b1;
// Should be semantically equivalent to a1 & b1
let a2;
let b2;
function fn1() { a1 = b1; } // this should work, but doesn't
function fn3() { a2 = b2; }
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

Activity

  1. jcalz commented on Feb 26, 2020

    @jcalz
    Contributor

    Haven't checked if this is a duplicate yet but whatever the issue is doesn't seem to have to do with discriminated unions (or even necessarily unions) or indexed access either per se:

    type Foo = {
        "a": unknown
        "b": string;
    }
    
    type Bar<T extends keyof Foo> = {
        type: Foo[T];
    }
    
    const err: Bar<"a"> = null! as Bar<"b">; // error
    // Bar<"b"> is not assignable to Bar<"a">
    
    const okay: { type: Foo["a"] } = null! as { type: Foo["b"] }; 

    Playground

  2. RyanCavanaugh commented on Feb 26, 2020

    @RyanCavanaugh
    Member

    Not sure why we're not falling back to structural comparison here.

    Workaround

    type TinyId<T extends keyof TinyModel> = T extends keyof TinyModel ? { type: TinyModel[T] } : never;
    
  3. ahejlsberg commented on Feb 26, 2020

    @ahejlsberg
    Member

    I believe the issue here is that we measure indexed access types such as TinyModel[T] as invariant, and since TinyId<T> contains a property of such a type, we consider it to be invariant in T. Effectively our variance measurement is more conservative than a structural check in this case. We do have provisions for tagging measurements as unreliable (which causes a follow-up structural check in the failure case), but we only do that for certain mapped types and rest parameters. We could try to do it for indexed access types as well, but it may have undesirable performance impact.

  4. ahejlsberg commented on Feb 26, 2020

    @ahejlsberg
    Member

    Wesley Wigham (@weswigham) Don't recall if you've experimented with this before.

  5. weswigham commented on Feb 27, 2020

    @weswigham
    Member

    TinyModel[T] is only covariant in T - but "a" and "b" are obviously not covariantly related; the issue is that variance reasoning about how T is possibly related between instances does not include any information from the constraint of TinyModel[T] as a whole (which requires information about TinyModel to be known). I don't think I've directly experimented with it, but adding Unreliable markers to access positions when the object type is non-generic should allow the structural fallback, yeah.

  6. mmorearty commented on Feb 27, 2020

    @mmorearty
    Contributor

    It's also worth mentioning that the compiler behavior changed at some point. The code in the playground example compiles in TS 3.3.3, but not in TS 3.5.1. (Also, thanks for the workaround, Ryan Cavanaugh (@RyanCavanaugh)!)

  7. ahejlsberg commented on Feb 28, 2020

    @ahejlsberg
    Member

    Wesley Wigham (@weswigham) I does seem that when a type variable is used as the index type in an indexed access type, its variance effectively becomes unmeasurable (because a relation between property names implies nothing about a relation between the types of those properties).

  8. jchurchill commented on Mar 8, 2020

    @jchurchill

    I've found a simpler workaround that doesn't involve conditional types: the type indexing must occur external to the definition of the object's shape. As an example, here's a slight modification to fix Joe Calzaretta (@jcalz) 's example:

    type Foo = {
        "a": unknown
        "b": string;
    }
    
    type BarInner<T> = { type: T };
    type Bar<T extends keyof Foo> = BarInner<Foo[T]>;
    
    const err: Bar<"a"> = null! as Bar<"b">; // no longer an error!
  9. ahejlsberg commented on Oct 26, 2020

    @ahejlsberg
    Member

    I'm closing this as a design limitation. The core issue is that we conservatively measure most indexed access types as invariant, where structural comparisons might be less restrictive. For example, see my comment here.

  10. added
    Design LimitationConstraints of the existing architecture prevent this from being fixed
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    RescheduledThis issue was previously scheduled to an earlier milestone
    on Oct 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Design LimitationConstraints of the existing architecture prevent this from being fixed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions