Skip to content

Resolve deferred conditional types to their true branch when instantiated with a type parameter constrained to the tested type #52144

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
kaczor6418 opened this issue Jan 8, 2023 · 5 comments
Labels
Experience Enhancement Noncontroversial enhancements Experimentation Needed Someone needs to try this out to see what happens Suggestion An idea for TypeScript
Milestone

Comments

@kaczor6418
Copy link

Bug Report

πŸ”Ž Search Terms

conditional-type, distributive-type, generics

πŸ•— Version & Regression Information

Version 4.9.4 and nightly

⏯ Playground Link

TS resolves type to different type even if I add constraint that should resolve to given type

πŸ’» Code

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t: number) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
      someFn(this.value);
    }
  }
}

In the above code I have created a PrimitiveDataType which is a union of all primitive data types in JavaScript. Then I have created a ConditionalType<T> that will resolve to some callback only if T is one of PrimitiveDataType. Then I have created an abstract generic class that have a field which type(ConditionalType<T>) depends on generic value of this class. In the end, I have crated a SomeClass that extends AbstractClass<T> and add constraints that generic parameter T have to extend PrimitiveDataType.

πŸ™ Actual behavior

For someMethod of this SomeClass conditionalFunctions.keys() is resolved to (v: T, t: number) => void what would implicate that my generic T extends an Array type.

πŸ™‚ Expected behavior

For someMethod of this SomeClass I have expected that TS should resolve conditionalFunctions.keys() to (v: T) => void to my surprise it is resolved to (v: T, t: number) => void what would implicate that my generic T extends an Array type. This doesn't make sens to me because T has constraint that has to extends PrimitiveDataType.

Example of what I would expect:

Works for regular variables

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t: number) => void
  : never;

const x: PrimitiveDataType = 12;
const y: ConditionalType<typeof x> = (param: number) => undefined;

StackOverflow question

@RyanCavanaugh
Copy link
Member

What's actually happening here is that the conditional type stays deferred. In its deferred from, you can still call it, as long as you provide an argument list that would satisfy every possible branch.

The reason it's deferred is that, while it would be safe to resolve to the true branch, for a generic it's not safe to resolve to the false branch because the type parameter might be instantiated with some more-specific type that actually does go to the true branch.

We don't yet have a concept of a "partial deferral"; it would be interesting to try. It'd be pretty tricky.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Experience Enhancement Noncontroversial enhancements Experimentation Needed Someone needs to try this out to see what happens labels Jan 12, 2023
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jan 12, 2023
@RyanCavanaugh RyanCavanaugh changed the title ConditionalType resolves to different type than expected Resolve deferred conditional types to their true branch when instantiated with a type parameter constrained to the tested type Jan 12, 2023
@fatcerberus
Copy link

In its deferred from, you can still call it, as long as you provide an argument list that would satisfy every possible branch.

Is this still true? I recall being told that this logic had been removed, which seems to be corroborated by the discussion in #51488

@kaczor6418
Copy link
Author

In its deferred from, you can still call it, as long as you provide an argument list that would satisfy every possible branch.

Is this still true? I recall being told that this logic had been removed, which seems to be corroborated by the discussion in #51488

I think this describes a bit different situation because in your example you are not getting rid of possible distributive type. In this playground, you can find fixed example that you have mentioned in #51488

@kaczor6418
Copy link
Author

What's actually happening here is that the conditional type stays deferred. In its deferred from, you can still call it, as long as you provide an argument list that would satisfy every possible branch.

The reason it's deferred is that, while it would be safe to resolve to the true branch, for a generic it's not safe to resolve to the false branch because the type parameter might be instantiated with some more-specific type that actually does go to the true branch.

We don't yet have a concept of a "partial deferral"; it would be interesting to try. It'd be pretty tricky.

Can I help with this somehow? Maybe you can give me a hint how to start work with this repository and some hint where I can start investigating this issue?

@CraigMacomber
Copy link

I think there is something a bit more subtle going on here. TypeScript actually does reduce distributive conditionals based on extends constraints. This is actually a questionable thing to do as some types (at least any) are allowed under such a constraint that do not evaluate to true.

What the example above does however is use a non-distributive conditional. TypeScript does not reduce this case based on the extends constraint on the generic type parameter. As far as I can tell this case should actually be more valid to do (since it handles types like any on the true branch) but it has not be implemented.

Here is a example on the playground showing the behavior.

Code:

/**
 * Compile time assert that A is assignable to (extends) B.
 * To use, simply define a type:
 * `type _check = requireAssignableTo<T, Expected>;`
 */
export type requireAssignableTo<_A extends B, B> = true;

function _generic1<T extends 1>() {
    type A = CheckA<T>;
    //   ^?
    
    // A is assignable to `true` even though it prints as a conditional (above)
    type _a = requireAssignableTo<A, true>; // Passes.
    // This seems to suggest that all possible values of T would result in A being true but this is incorrect: CheckA<any> is `boolean` for example.
    // This inconsistancy is likley related to the fact that the extends clause on T in this function is not distribuative, but the compiler is assuming that it can be used to infer the outcome of the distributive one in CheckA.

    // To be more strict we can use CheckB which should more robustly return true in all cases by avoiding distributing over conditionals. 
    type B = CheckB<T>;
    //   ^?

    // B is not assignable to `true`, despite all possible types which could be provided for T resulting in B being true.
    // It would be nice it the TypeScript compiler could reduce this conditional, which is always actually true, to true.
    // Fixing this could prabably be done by making the evaluation logic which reduces the conditional in the A case above recognize the non-distributive extends pattern recommended in the handbook (and that case would actually be more forrect that the existing one)
    type _b = requireAssignableTo<B, true>; // Fails.
}

// True if T extends 1: Distributive
// Distributive version of CheckB, see https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
// Sutable for handeling types in contexts which should behave covariently.
type CheckA<T> = T extends 1 ? true : false;

// True if T extends 1: Non-distributive
// Non-distributive version of CheckA, see https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
// Sutable for handeling types in contexts which should behave contravariently or invariently.
type CheckB<T> = [T] extends [1] ? true : false;

{
    type testA = CheckA<any>;
    //   ^?
    type testB = CheckB<any>;
    //   ^?
}



// It's is somewhat possible to work around this limitation in some cases by adding and extra [T] type paramater to everything that could be used in a generic context, then plumbing it through to the places that perform the extends checks.
// This however is really messy and not always practical.
function _generic2<T extends 1, T2 extends [1] & [T] = [T]>() {
    type B = CheckB3<T, T2>;
    //   ^?
    type _b = requireAssignableTo<B, true>;
}

type CheckB3<T, T2 extends [T] = [T]> = T2 extends [1] ? true : false;

I also made a simplified version of the example from this tread showing the difference: Playground

CraigMacomber added a commit to microsoft/FluidFramework that referenced this issue Mar 11, 2025
## Description

This adds some tests which are generically parameterized over schema
types. These are inspired by ongoing experiments with a Table schema
generator, and issues encountered in its implementation.

Generally it seems that we have some cases where we would really like
TypeScript to simplify conditional type expressions based on extends
clauses, but it does not do so causing code which would otherwise type
check to fail. One such cause is covered in
microsoft/TypeScript#52144 (comment)
but there are likely others.
chentong7 pushed a commit to chentong7/FluidFramework that referenced this issue Mar 11, 2025
## Description

This adds some tests which are generically parameterized over schema
types. These are inspired by ongoing experiments with a Table schema
generator, and issues encountered in its implementation.

Generally it seems that we have some cases where we would really like
TypeScript to simplify conditional type expressions based on extends
clauses, but it does not do so causing code which would otherwise type
check to fail. One such cause is covered in
microsoft/TypeScript#52144 (comment)
but there are likely others.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Experimentation Needed Someone needs to try this out to see what happens Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants