Skip to content

Non-naked type parameter match still has distributive behavior #43727

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
strblr opened this issue Apr 19, 2021 · 7 comments
Closed

Non-naked type parameter match still has distributive behavior #43727

strblr opened this issue Apr 19, 2021 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@strblr
Copy link

strblr commented Apr 19, 2021

Bug Report

🔎 Search Terms

Generic types, distributive unions, extends, naked type parameter

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about distributivity with naked type parameters.

⏯ Playground Link

Playground link with relevant code

💻 Code

type A<T> = T;
type B<T> = A<T> extends true ? "X" : "Y";
type C = B<true | false>; // "X" | "Y" (??)

🙁 Actual behavior

A<T> in B distributes types and returns the union of the results.

🙂 Expected behavior

Since T is not naked in A<T>, I was expecting distributivity to go away, like in :

type A<T> = T extends infer U ? U : never;
type B<T> = A<T> extends true ? "X" : "Y";
type C = B<true | false>; // "Y"
@jcalz
Copy link
Contributor

jcalz commented Apr 19, 2021

Presumably this is going to turn out to be something like "A<T> gets replaced with T inside the definition of B<T> before the conditional type is checked", but it's definitely surprising. On the face of it, A<T> extends true ? "X" : "Y" doesn't look like it should be distributive no matter what A<T> is defined to be. But what do I know?

I feel like I've seen a similar reported issue before but I can't locate it.

@RyanCavanaugh
Copy link
Member

Yeah, the check here is whether the check type is semantically a type parameter, not syntactically. Changing this now just makes it surprising to a different set of people and would be a breaking change, so I think this is working as intended.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Apr 19, 2021
@jcalz
Copy link
Contributor

jcalz commented Apr 19, 2021

I'm not 100% sure I get what you mean by "semantically" here. I can imagine this having to do with details of when and whether the compiler reduces the checked type to a bare type parameter. For example:

type D<T> = (T & unknown) extends true ? "X" : "Y"
type E = D<boolean>; // "Y" | "X", distributive

type F<T> = { prop: T }['prop'] extends true ? "X" : "Y"
type G = F<boolean> // "Y" | "X", distributive

type H<U extends { x: any }> = U['x'] extends true ? "X" : "Y";
type I<T> = H<{ x: T }> // IntelliSense says: T extends true ? "X" : "Y", but:
type J = I<boolean> // "Y", 𝗻𝗼𝘁 distributive

The conditional types in D<T> and F<T> are distributive, apparently because T & unknown and {prop: T}['prop'] are eagerly reduced to T before the conditional type is considered. But while I<T> is "semantically" checking the bare type parameter T (at least to my understanding of that term), it is not distributive... most likely because U['x'] remains unaltered when the conditional type is evaluated.

I don't necessarily think any of this behavior is a bug, but the only way I can make sense of this in my head is to put aside my conception of semantics and instead focus on order of operations (despite the fact that I only have a dim sense of what this is).

@RyanCavanaugh
Copy link
Member

The relevant code looks like this:

        function getTypeFromConditionalTypeNode(node: ConditionalTypeNode): Type {
            const links = getNodeLinks(node);
            if (!links.resolvedType) {
                const checkType = getTypeFromTypeNode(node.checkType);
                const aliasSymbol = getAliasSymbolForTypeNode(node);
                const aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol);
                const allOuterTypeParameters = getOuterTypeParameters(node, /*includeThisTypes*/ true);
                const outerTypeParameters = aliasTypeArguments ? allOuterTypeParameters : filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node));
                const root: ConditionalRoot = {
                    node,
                    checkType,
                    extendsType: getTypeFromTypeNode(node.extendsType),
                    isDistributive: !!(checkType.flags & TypeFlags.TypeParameter),
                     ^^^^^^^^^

In D, T & unknown is just another way to write T -- intersection immediately reduces types of this form to their non-unknown form.

In F, there's nothing that needs to be deferred, so we have a roundabout way of referencing the type parameter T.

In H, U['x'] is a lookup type, not a type parameter, so it's not distributive.

The distinctions are largely outside the range of what anyone would normally learn, since any form that can immediately reduce to a type parameter is a form that "isn't doing anything" in the first place and thus unlikely to be written.

@strblr
Copy link
Author

strblr commented Apr 19, 2021

I have a question that might sound naive but what's the case for distributivity? I stumbled upon it while coding, it felt like a bug, and I have to use what feels like artificial workarounds like [T] extends [never] ? true : false. I'm curious as to what motivated the introduction of that concept into TS in the first place. Thank you.

@RyanCavanaugh
Copy link
Member

Most conditional types benefit from distributivity, e.g. Exclude<"a" | "b", "b"> is "a", not never

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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

4 participants