Skip to content

Conditional type on type argument reports different value when assigned to vs assigned from #52021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
calebmer opened this issue Dec 26, 2022 · 1 comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Milestone

Comments

@calebmer
Copy link

Bug Report

πŸ”Ž Search Terms

Conditional type, assigned to, assigned from, type arguments

πŸ•— Version & Regression Information

  • This is a crash: no
  • This changed between versions 4.2.3 and 4.3.5

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

function test<T extends {}>() {
    type T1 = T extends {} ? true : false;
    type T2 = T & {} extends {} ? true : false;

    const t1a: T1 = true; // Errors?
    const t2a: T2 = true; // Ok

    const t1b: true = make<T1>(); // Ok
    const t2b: true = make<T2>(); // Errors?
}

declare function make<T>(): T;

πŸ™ Actual behavior

  1. true is not assignable to T1.
  2. T2 is not assignable to true.

πŸ™‚ Expected behavior

  1. true is assignable to T1.
  2. T2 is assignable to true.
@Andarist
Copy link
Contributor

Aren't cases like this just fun? πŸ˜…

The first thing that we need to take into account when discussing this is that conditional types with generic checkType or extendsType are not resolved (and that could lead to simplifying them to their trueType/falseType). There is a comment about this here:

// We attempt to resolve the conditional type only when the check and extends types are non-generic

On top of that, we have 2 similar sets of assignments in your examples. In each set we either try to assign a conditional type to primitive type or vice-versa, we try to assign a primitive to a conditional type. One of the conditional types used there is distributive (T1) while the other one isn't (T2).

const t1a: T1 = true;

In here, we have a conditional type at the target position and this conditional type is detected as distribution dependent here. This is the main reason why this fails as it's the main part of the code that can allow anything to be assignable to a conditional type (the only other part that allows this, and that I've found, is when the source type is a conditional type as well).

I was surprised that this conditional type is detected as dependent on distribution though. I dug a little bit and prepared a PR that fixes (?) this: #52034 .

However, even with that potential fix we still don't get "expected" results. skipFalse is not being computed to true here and thus the conditional type is not "simplified" to its true branch as we end up checking if true is assignable to false here.

This whole skipFalse check is based on checking assignability of "restrictive instantiations" of the checkType and extendsType. I don't understand this concept at all (nor I understand "permissive instantiations"). The only thing that I know is that it doesn't grab the constraint of a type parameter - it uses the noConstraintType instead. This can be seen here

const t1b: true = make<T1>();

This is the opposite situation - the conditional type is the source and not the target. In such case, we successfully relate them here. The conditional type gets simplified as the constraint of the checkType ({}) is grabbed here and the conditional type is instantiated using this. So the true type gets returned from here

const t2a: T2 = true;

In here, we right away go to the block that deals with permissive/restrictive instantiations as this conditional type is not distributive (and thus it also isn't distribution dependent).

The restrictive instantiation of the checkType is still equivalent to T & {} and thus, based on intersection rules, this assignable to the restrictive instantiation of the extendsType ({}). For intersections it's enough to check if any of the intersected members is assignable to the target and one of the members is {}. Based on that we "simplify" the conditional type with skipFalse === true.

const t2b: true = make<T2>();

Since this conditional type is not distributive we end up checking if its default constraint is assignable to the target (here). The default constraint of a conditional type is a union of both branches (no simplification happening here), this can be seen here. So we end up with boolean (true | false) here and that is not assignable to true.


I simply describe why this all happens at the implementation level. I would expect that some improvements around this can be made.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jan 3, 2023
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jan 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

3 participants