Skip to content

Conditional Types and Distribution over Union #21870

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

Closed
jack-williams opened this issue Feb 11, 2018 · 2 comments
Closed

Conditional Types and Distribution over Union #21870

jack-williams opened this issue Feb 11, 2018 · 2 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 11, 2018

I don't believe this is a bug, but due to the design of distribution over unions in the conditional. In some cases I think it would be more intuitive to treat the union atomically, in particular when the union is meant to be a key set.

TypeScript Version: 2.8.0

Search Terms: Conditional Types

Code

type Thing = {
    a: number,
    b: string,
    c: boolean,
};

type PickOrNever<T, K> = K extends keyof T ? Pick<T, K> : never;

type AB = PickOrNever<Thing, "a" | "b">;

Desired behavior:

AB = Pick<Thing, "a" | "b"> = { a: number; b: string }

Actual behavior:

AB = Pick<Thing, "a"> | Pick<Thing, "b"> = { a: number} | { b: string }

The context where I stumbled across this was trying to define a pick function using tuples of fields, rather than a list of union type. So I wanted something like:

type TuplePick<T, F> =
    F extends [infer R, infer R] ?
    (R extends keyof T ? Pick<T, R> : never) :
    never

Assuming that there is not an alternate encoding and this is a use case worth supporting, two possible solutions:

  1. Do not have conditional types distribute when the condition is of the form X extends keyof Y.
  2. Be able to box variables to prevent lifting. e.g. type PickOrNever<T, K> = [K] extends [keyof T] ? Pick<T, K> : never; Currently this wont work as keyof does not propagate from inside the tuple to the bare variable K. (K does not satisfy the constraint keyof T).
@ahejlsberg
Copy link
Member

This is indeed an effect of automatically distributing over unions. Here's how you could write it:

type PickOrNever<T, K> = [K] extends [keyof T] ? Pick<T, Extract<keyof T, K>> : never;

If you're fine with ignoring excess keys (instead of producing never when any are present), you can just simplify down to:

type PickExisting<T, K> = Pick<T, Extract<keyof T, K>>;

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 11, 2018
@jack-williams
Copy link
Collaborator Author

jack-williams commented Feb 11, 2018

Thanks for the reply!

I figured there was a trick I was missing and it was convincing the checker that K extends keyof T in the true branch of the conditional for the constraint in Pick.

I think this works by building the selected keys from Extract which only ever returns types from keyof T, so the result is trivially assignable to keyof T. Under the assumption K extends keyof T then Extract<keyof T, K> = K, I think.

Is the fact that extends does not propagate to the true branch when guarded by [] down to design choices, for soundness etc, or just the way the checker works.

EDIT: I think my question and example could be clearer. I guess what I was wondering was whether

  1. Does [A] extends [B] imply A extends B?
  2. Assuming 1. could those constraints propagate in conditional types?

E.g.

type Extender<T, K extends T> = K;
type ExtenderTest<T, K> = K extends T ? Extender<T, K> : never // OK
type ExtenderTestBoxed<T, K> = [K] extends [T] ? Extender<[T], [K]> : never // NOT-OK
/*
Type '[K]' does not satisfy the constraint '[T]'.
  Type 'K' is not assignable to type 'T'. [2344]
*/

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants