1

I apologize for the poor title, but I don't have the vocabulary to word it better. Let me show you my code:

type Foo = "bar" | "baz";

type Consistency  = {
    [K in Foo]: {
        foo: K;
        fooTemplate: `${K} in a template`;
    }
}[Foo]

// I want this to compile (and it does)
const valid1: Consistency = {
    foo: "bar",
    fooTemplate: "bar in a template",
}

const valid2: Consistency = {
    foo: "baz",
    fooTemplate: "baz in a template",
}

export const anFoo: Foo = "bar"

const valid3: Consistency = {
    foo: anFoo,
    fooTemplate: `${anFoo} in a template` as const
}

export interface MyObj {
  temporary: {    
    myProperty: Foo;    
  };
}
const state: MyObj = {
    temporary: {
        myProperty: "bar" 
    }
}
const i: Foo = state.temporary.myProperty;
const valid4: Consistency = { // Why doesn't this one compile????
    foo: i,
    fooTemplate: `${i} in a template` as const
}

// I don't want this to compile (and it doesn't)
const invalid1: Consistency = {
    foo: "bar",
    fooTemplate: "baz in a template",
}

const invalid2: Consistency = {
    foo: "baz",
    fooTemplate: "bar in a template",
}

You can play with this here. This all works the way I expect accept for valid4. For some reason, valid4 doesn't compile, and I want it to. Why isn't it compiling and how do I make it compile?


The answer doesn't explicitly say this so I will: There's no solution to this problem.

2
  • 1
    I've seen this error before where a union is not assignable to itself because the union is not assignable to either individual member. I bet @jcalz can come along and explain it better than I can. You have i and you as the author know that it's the same i in both places, but TS is just looking at i as type Foo. It is possible to have a type Foo in both places and not be assignable to Consistency if one Foo is "bar" and the other Foo is "baz". so the type Foo cannot guarantee assignability. Commented Feb 9, 2021 at 21:54
  • 1
    It's not about nested objects, it's just that anFoo is typed as "bar" and not as Foo. declare const anFoo: Foo doesn't work either. Commented Feb 9, 2021 at 22:09

1 Answer 1

2

I believe the answer is this, but maybe Jcalz can correct me.

Unions of "A" | "B" actually have 3 assignability's.

"A" is assignable. "B" is assignable. And "A" | "B" is assignable

Its this third case that breaks valid4. At a value level this is impossible, but at a type level its possible that its both "bar" | "baz" and therefore it thinks...

{foo: "baz", fooTemplate: "bar as template"}

is possible.

As for valid3, this is because Typescript compiler can detect you've hardcoded the value there and therefore even though you're asserting "Foo" the actual type it considers that is "bar"

Sign up to request clarification or add additional context in comments.

3 Comments

This looks right to me... the compiler mostly doesn't care about variables per se, but about the types of the variables. There's no great way to have the compiler follow correlations between two union-typed values automatically; you have to drag the compiler through the separate cases like i === "bar" ? { foo: i, fooTemplate: `${i} in a template` as const } : { foo: i, fooTemplate: `${i} in a template` as const }, which looks like another instance of this problem.
@jcalz since that links to an open issue, does that mean there's no solution?
There's no perfect solution, no. There are workarounds, of course: either use redundant code like the above ternary and get type safety, or use a type assertion like { foo: i, fooTemplate: `${i} in a template` as const } as Consistency; and get convenience. You can't have both, as far as I know.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.