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

Avoid distribution of conditional types over union types #29368

Closed
2 tasks
aloifolia opened this issue Jan 11, 2019 · 4 comments
Closed
2 tasks

Avoid distribution of conditional types over union types #29368

aloifolia opened this issue Jan 11, 2019 · 4 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@aloifolia
Copy link

Search Terms

Suggestion

I would like to disable the distribution of union types over conditional types in certain cases. If this is already possible (not sure about that) it should be documented in a better way.

Use Cases

Suppose you would like to infer type parameters like so:

type Route = {
    Path: string;
    Params: { [key: string]: string }
}

type RouteWithParams<T extends string> = {
    Path: string;
    Params: { [key in T]: string }
}

type RouteWithParamsMaker<T extends Route> = (keyof T['Params']) extends infer U
    ? U extends string
        ? RouteWithParams<U>
        : never
    : never;

function toRouteWithParams<T extends Route>(r: T): RouteWithParamsMaker<typeof r> {
    return (r as unknown) as RouteWithParamsMaker<typeof r>;
}

const route = {
    Path: 'somewhere',
    Params: { id: 0, name: 1 },
}

const routeWithParams = toRoute(route);

The resulting variable is currently inferred to have the type

RouteWithParams<"id"> | RouteWithParams<"name">

Instead, I would like to receive the following type

RouteWithParams<"id" | "name">

I have no idea whether the current behaviour is intentional (and we would need a new annotation for such cases) or a bug.

Checklist

My suggestion meets these guidelines (not sure about the first and the last one):

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x ] This wouldn't change the runtime behavior of existing JavaScript code
  • [x ] This could be implemented without emitting different JS based on the types of the expressions
  • [x ] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@aloifolia
Copy link
Author

So.... I just found out that I actually can circumvent the distribution if I change RouteWithParamsMaker:

type RouteWithParamsMaker <T extends Route> = (keyof T['Params']) extends infer U
    ? RouteWithParams<Extract<U, string>>
    : never;

This came a bit as a surprise - my initial feeling was that it should behave equally. So I guess one way to avoid the distribution is to extract conditionals in a clever way to new types. I would really like to have better guidance as a programmer here.

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 11, 2019

To avoid distribution: wrap any naked type parameters in a 1-tuple, and do the same for the check type.

type RouteWithParamsMaker<T extends Route> = (keyof T['Params']) extends infer U
    ? [U] extends [string]
        ? RouteWithParams<U>
        : never
    : never;

I don't believe this is in the TypeScript handbook, but it probably should be.

Tag for searching purpose: distributive conditional type trouble

@weswigham weswigham added the Question An issue which isn't directly actionable in code label Jan 11, 2019
myme pushed a commit to myme/jsxapi that referenced this issue Jan 19, 2020
Conditional types are distributed across all types of a union. This is
not what we want in the case of Config. Distribution happens only over
"naked" type parameters and so wrapping the type in a 1-tuple as well as
the check type avoids this behavior. See:

microsoft/TypeScript#29368 (comment)

Before this change, the type of leaf nodes would resolve to:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False'> & Settable<'False'> & Listenable<'False'>
    ) | (
      Gettable<'True'> & Settable<'True'> & Listenable<'True'>
    )
  }
}

whereas what we want is:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False' | 'True'> &
      Settable<'False' | 'True'> &
      Listenable<'False' | 'True'>
    )
  }
}
myme pushed a commit to myme/jsxapi that referenced this issue Jan 20, 2020
Conditional types are distributed across all types of a union. This is
not what we want in the case of Config. Distribution happens only over
"naked" type parameters and so wrapping the type in a 1-tuple as well as
the check type avoids this behavior. See:

microsoft/TypeScript#29368 (comment)

Before this change, the type of leaf nodes would resolve to:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False'> & Settable<'False'> & Listenable<'False'>
    ) | (
      Gettable<'True'> & Settable<'True'> & Listenable<'True'>
    )
  }
}

whereas what we want is:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False' | 'True'> &
      Settable<'False' | 'True'> &
      Listenable<'False' | 'True'>
    )
  }
}
myme pushed a commit to myme/jsxapi that referenced this issue Apr 27, 2020
Conditional types are distributed across all types of a union. This is
not what we want in the case of Config. Distribution happens only over
"naked" type parameters and so wrapping the type in a 1-tuple as well as
the check type avoids this behavior. See:

microsoft/TypeScript#29368 (comment)

Before this change, the type of leaf nodes would resolve to:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False'> & Settable<'False'> & Listenable<'False'>
    ) | (
      Gettable<'True'> & Settable<'True'> & Listenable<'True'>
    )
  }
}

whereas what we want is:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False' | 'True'> &
      Settable<'False' | 'True'> &
      Listenable<'False' | 'True'>
    )
  }
}
myme pushed a commit to myme/jsxapi that referenced this issue Apr 27, 2020
Conditional types are distributed across all types of a union. This is
not what we want in the case of Config. Distribution happens only over
"naked" type parameters and so wrapping the type in a 1-tuple as well as
the check type avoids this behavior. See:

microsoft/TypeScript#29368 (comment)

Before this change, the type of leaf nodes would resolve to:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False'> & Settable<'False'> & Listenable<'False'>
    ) | (
      Gettable<'True'> & Settable<'True'> & Listenable<'True'>
    )
  }
}

whereas what we want is:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False' | 'True'> &
      Settable<'False' | 'True'> &
      Listenable<'False' | 'True'>
    )
  }
}
myme pushed a commit to myme/jsxapi that referenced this issue Aug 14, 2020
Conditional types are distributed across all types of a union. This is
not what we want in the case of Config. Distribution happens only over
"naked" type parameters and so wrapping the type in a 1-tuple as well as
the check type avoids this behavior. See:

microsoft/TypeScript#29368 (comment)

Before this change, the type of leaf nodes would resolve to:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False'> & Settable<'False'> & Listenable<'False'>
    ) | (
      Gettable<'True'> & Settable<'True'> & Listenable<'True'>
    )
  }
}

whereas what we want is:

{
  Bluetooth: {
    Allowed: (
      Gettable<'False' | 'True'> &
      Settable<'False' | 'True'> &
      Listenable<'False' | 'True'>
    )
  }
}
@ClickerMonkey
Copy link

If you don't have a type to test against, [T] extends [T] will have the same effect.

This may be useful for others.

type NoDistribute<T> = [T] extends [T] ? T : never;

However, if T was used in a previous conditional type it may have already been distributed. If you have a deeply nested type you can use the single-element-tuple trick at each level around types you don't want distributed.

@yss14
Copy link

yss14 commented Apr 21, 2022

How to handle any type with single tuple type solution?

type NotUndefinedAndNotVoid<T> = [T] extends [undefined]
  ? never
  : [T] extends [void]
  ? never
  : T | Promise<T>

type A = NotUndefinedAndNotVoid<any> // A is never since "[any] extends [undefined]" evaluates to true

type NotUndefinedAndNotVoidDistributed<T> = T extends undefined
  ? never
  : T extends void
  ? never
  : T | Promise<T>

type B = NotUndefinedAndNotVoidDistributed<any> // B is any because "any extends undefined" evaluates to false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants