Skip to content
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

T should be assignable to A extends B ? C : D if it is assignable to both C and D #26933

Closed
4 tasks done
mattmccutchen opened this issue Sep 6, 2018 · 10 comments · Fixed by #30639
Closed
4 tasks done
Assignees
Labels
Fix Available A PR has been opened for this issue Investigating Is in active investigation Suggestion An idea for TypeScript

Comments

@mattmccutchen
Copy link
Contributor

mattmccutchen commented Sep 6, 2018

Search Terms

conditional assignable both

Suggestion

A type T should be assignable to A extends B ? C : D if it is assignable to both C and D. Previously mentioned here and here, but it doesn't appear that anyone ever filed an issue to track this.

It's unclear what to do if C references infer type parameters, but even an assignability rule that works only when C does not reference infer type parameters would be useful.

If the conditional type is distributive, then the rule only applies if A is known not to be never.

Use Cases

See this comment for one.

Examples

TBD

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@jack-williams
Copy link
Collaborator

In the comment under use cases this is given as an example:

type Foo<T> = T extends true ? string : "a";

function test<T>(x: Foo<T>, s: string) {
    x = "a";  // Currently an error, should be ok
    x = s;    // Error
}

But am I right in saying that if T is instantiated to never, then Foo<never> = never, so it is not safe to assign "a". Are there some hidden constraints I am missing?

@mattmccutchen
Copy link
Contributor Author

@jack-williams Good catch. That example is wrong, but I believe the Something<T> example in the middle of the same comment is valid. I updated the proposal.

@jack-williams
Copy link
Collaborator

It's unclear what to do if C references infer type parameters, but even an assignability rule that works only when C does not reference infer type parameters would be useful.

One possibility might be (ignoring distributive conditional types):

  • T is assignable to A extends B ? C : D if T is assignable to C[-unknown, +never] & D

where C[-unknown, +never] represents the type C with all covariant infer types substituted for never, and all contravariant infer types substituted for unknown.

Though I agree that a rule that works for closed types (without infer) would still be very useful and probably the more suitable option.

If the conditional type is distributive, then the rule only applies if A is known not to be never.

Is there a way to cleanly flag this to the typechecker?

@mattmccutchen
Copy link
Contributor Author

mattmccutchen commented Sep 6, 2018

  • T is assignable to A extends B ? C : D if T is assignable to C[-unknown, +never] & D

We may as well set the contravariant occurrences to their constraints, not to unknown. What about invariant occurrences? (I know that if you expand generic type references without limit, then there are no invariant occurrences, but by the time you make such an implementation terminating, it would be tantamount to just reasoning about the infer variables as unknowns.) This led me to realize that the current assignability rule for A extends B ? C : D to T, which sets all infer occurrences to their constraints, is unsound; I filed #26945 for that.

Is there a way to cleanly flag this to the typechecker?

We'd just specify this for each built-in type constructor. E.g., a primitive type, an object type, or unknown is OK, a union containing at least one OK type is OK (although it should have already been distributed), never is not OK, and (for a simple implementation) anything "instantiable" (e.g., a type parameter or an unsimplified lookup type or conditional type containing type parameters) is not OK.

@jack-williams
Copy link
Collaborator

We may as well set the contravariant occurrences to their constraints, not to unknown.

Yep, good point.

What about invariant occurrences?

In that case I think it has to be an error because we're left trying to guess the exact type the parameter will be instantiated to.

We'd just specify this for each built-in type constructor. E.g., a primitive type, an object type, or unknown is OK, a union containing at least one OK type is OK (although it should have already been distributed), never is not OK, and (for a simple implementation) anything "instantiable" (e.g., a type parameter or an unsimplified lookup type or conditional type containing type parameters) is not OK.

Makes sense to me. I'm mainly just wondering if there is a way to signal a distributive conditional type will never be short circuited with never because that case is handled. E.G.

type Foo<A> = [A] extends [never] ? C : A extends B ? C : D;

I don't really have enough experience to say whether a distributive conditional type that does not short-circuit is useful in practice, but from an assignability POV it makes things easier.

@mattmccutchen
Copy link
Contributor Author

Enough people seem to be running into this, let's get something basic in place. I submitted #27589.

@s0
Copy link
Contributor

s0 commented Oct 9, 2020

Could we extend the keywords of this issue with deferred assignment?, thanks!

@mattmccutchen
Copy link
Contributor Author

@s0 I think you've just done so.

@s0
Copy link
Contributor

s0 commented Oct 9, 2020

@s0 I think you've just done so.

derp. comments are searchable, you're right! 😂

@ayroblu
Copy link

ayroblu commented Dec 6, 2020

Adding an example just to make the point clear, I hope this can be looked at as this is quite an old issue

type Param<T> = T extends undefined ? {
    first: string,
} : {
    first: string,
}
function run<T>(param: T) {
    const p: Param<T> = { // error assigning here
        first: 'hi'
    }
    return p;
}

Playground link

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment