Skip to content

[true, false] extends [false, false] sometimes it's true(when use infer,happen with a probability ), and sometime it's false(in most cases)??? #51090

Description

@Max10240

Bug Report

🔎 Search Terms

extends when infer, lazy evaluation

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________

⏯ Playground Link

Playground link with relevant code
more detail can also see: stackOverflow

💻 Code

type T0 = [IsNegative<-15>, IsNegative<90>]; // [true, false]
type T1 = Sub<-15, 90>; // ["Why?", [true, false]] /* Why [true, false] extends [false, false] ??? */

type T2 = [boolean, boolean] extends [false, false] ? true : false; // true
type T3 = [false, false] extends [boolean, boolean] ? true : false; // false

export type StartsWith<S extends string, SearchString extends string> = S extends `${SearchString}${infer T}` ? true : false;
export type IsNegative<N extends number> = StartsWith<`${N}`, '-'>;

export type Sub<A extends number, B extends number> = [IsNegative<A>, IsNegative<B>] extends infer R
    ? R extends [false, false]
        ? ['Why?', R]
        : 'Expected'
    : never;

export type Sub1<A extends number, B extends number> = [IsNegative<A>, IsNegative<B>] extends infer R
    ? [false, false] extends R
        ? ['Why?', R]
        : 'Expected'
    : never;

export type Sub2<A extends number, B extends number> = [IsNegative<A>, IsNegative<B>] extends [false, false]
    ? ['Why?',]
    : 'Expected'

export type Sub3<A extends number, B extends number> = [IsNegative<A>, IsNegative<B>][number] extends false
    ? ['Why?',]
    : 'Expected'

type T0 = [IsNegative<-15>, IsNegative<90>]; // [true, false]
type T1 = Sub<-15, 90>; // ["Why?", [true, false]] /* Why [true, false] extends [false, false] ??? */
type T2 = Sub1<-15, 90>; // Expected /* Work well */
type T3 = Sub2<-15, 90>; // ['Why?] /* Can't work too */
type T4 = Sub3<-15, 90>; // Expected /* Work well */


🙁 Actual behavior

type T1 = ["Why?", [true, false]]'

🙂 Expected behavior

type T1 = 'Expected' (Same as T2)

Activity

  1. DanielRosenwasser commented on Oct 7, 2022

    @DanielRosenwasser
    Member

    It's a bit of an aside, but in newer versions of TypeScript you can actually capture and constrain an infer type parameter.

    export type Sub2<A extends number, B extends number> =
        [IsNegative<A>, IsNegative<B>] extends (infer R extends [false, false])
            ? ['Why?', R]
            : 'Expected'
    
    export type Sub3<A extends number, B extends number> =
        [IsNegative<A>, IsNegative<B>][number] extends (infer R extends false)
            ? ['Why?', R]
            : 'Expected'
  2. Max10240 commented on Oct 7, 2022

    @Max10240
    Author

    Daniel Rosenwasser (@DanielRosenwasser) Yesinfer X extends SomeType is a great feature , but I want to cache the result of this calculation into variable R so that I don't have to re-evaluate it later, just like variables in a programming language

  3. Andarist commented on Oct 7, 2022

    @Andarist
    Contributor

    Daniel Rosenwasser (@DanielRosenwasser) A bit of an aside, but I've always wondered why TypeScript can't capture the constraint of an infer type parameter on its own 😅 would you mind shedding some light on this one?

  4. DanielRosenwasser commented on Oct 7, 2022

    @DanielRosenwasser
    Member

    I might be missing something. Let me try to answer both of these points

    but I want to cache the result of this calculation into variable R so that I don't have to re-evaluate it later

    I think that's what my example is doing - your first two examples had to do this by using two nested conditionals. Your second two examples seemed to drop the second tuple element - I assumed this was so that you could simplify with one conditional. I'm just mentioning that you can still capture the type with an infer R extends.

    A bit of an aside, but I've always wondered why TypeScript can't capture the constraint of an infer type parameter on its own

    Disclaimer: you might be more familiar with the checker than me at this point.

    Technically we propagate the implicit constraint through an internal kind of type called "substitution types", which you can sort of think of as intersection (and in a sense, you can think of intersections as an alternative model of constraining type parameters). Substitution types would need some sort of new notation, or we would have to just replace them with intersection types.

    Here's one reason I think we don't keep them around: If we captured these types, you'd start seeing intersections you didn't really write which could be an odd experience; plus, when you instantiate a type sufficiently for a conditional type to resolve, you don't need the substitution type anymore - so it's really better to substitute the type as it was written instead of writing an intersection which won't always be simplified away.

    I think there's another element of keeping the design space open. Right now substitution types are mostly an implementation detail. The way that they're used is to validate that a type variable satisfies the requirements in how they're used (e.g. with type Foo<T extends SomeType>, U is sufficiently constrained to be used in Foo<U>). But I said "in a sense, you can think of intersections as an alternative model of constraining type parameters". This is something we toy with from time to time - but are we set on the intersection representation? I'm not entirely sure.

    Others might have more thoughts here, and I'd welcome corrections or other thoughts.

  5. DanielRosenwasser commented on Oct 7, 2022

    @DanielRosenwasser
    Member

    I spoke with Anders Hejlsberg (@ahejlsberg) and I totally confused myself - I was thinking about substitution types from the left side of the extends. Substitution types don't come into play with infer type variables. Please disregard my comment. 😅

    Yes, we could possibly do something like that. It might be worth opening a separate issue for it.

  6. Max10240 commented on Oct 8, 2022

    @Max10240
    Author

    Daniel Rosenwasser (@DanielRosenwasser)

    I think that's what my example is doing

    Here's what I want to do:([IsNegative<A>, IsNegative<B>] only need to calculate it once, and you can use it many times later)

    export type Sub<A extends number, B extends number> = [IsNegative<A>, IsNegative<B>] extends (infer R extends [boolean, boolean])
        ? R extends [true, true]
            ? '--'
            : R extends [true, false]
                ? '-+'
                : R extends [false, true]
                    ? '+-'
                    : '++'
        : never;
    
    type T1 = Sub<-15, 90>; // '-+' work well
    
    export type Sub1<A extends number, B extends number> = [IsNegative<A>, IsNegative<B>] extends (infer R extends [boolean, boolean])
        ? R extends [false, false]
            ? '++'
            : R extends [true, false]
                ? '-+'
                : R extends [false, true]
                    ? '+-'
                    : '--'
        : never;
    
    type T2 = Sub1<-15, 90>; // '++' Oh no!, expected to '-+'

    Just switch the order of the judgment statements, and the result actually changes. Now I'm almost certain there's some kind of problem here.

  7. ahejlsberg commented on Oct 8, 2022

    @ahejlsberg
    Member

    This is working as intended. The issue is that the StartsWith<S, SearchString> type doesn't appropriately account for S being string. For example, StartsWith<string, '-'> produces false, which clearly isn't correct for all strings. Even though none of the examples pass string as a type argument, it happens during constraint evaluation when determining whether to resolve the conditional type or defer it.

    A better way to write the type is:

    export type StartsWith<S extends string, SearchString extends string> =
        string extends S ? boolean :
        S extends `${SearchString}${string}` ? true :
        false;

    This appropriately accounts for arbitrary strings by answering boolean, i.e. could be either false or true.

  8. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    BugA bug in TypeScript
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Oct 8, 2022
  9. Max10240 commented on Oct 9, 2022

    @Max10240
    Author

    Anders Hejlsberg (@ahejlsberg) Good explanation and even helped me find bugs in other places😉, thank you

  10. Max10240 commented on Oct 30, 2023

    @Max10240
    Author

    Anders Hejlsberg (@ahejlsberg) Sorry to ask you for help again, a similar problem arises, but this time it looks even weirder!

    type PositiveInfinity = 1e999;
    type NegativeInfinity = -1e999;
    
    export type IsEqual<A, B> =
    	(<G>() => G extends A ? 1 : 2) extends
    	(<G>() => G extends B ? 1 : 2)
    		? true
    		: false;
    
    
    export type Add<A extends number, B extends number> = [
    	IsEqual<A, PositiveInfinity>, IsEqual<A, NegativeInfinity>,
    	IsEqual<B, PositiveInfinity>, IsEqual<B, NegativeInfinity>,
    ] extends infer R extends [boolean, boolean, boolean, boolean]
    	? [true, false] extends ([R[0], R[3]]) // Note: with parentheses
    		? PositiveInfinity
    		: 'failed'
    	: never;
    
    export type AddWithoutParentheses<A extends number, B extends number> = [
    	IsEqual<A, PositiveInfinity>, IsEqual<A, NegativeInfinity>,
    	IsEqual<B, PositiveInfinity>, IsEqual<B, NegativeInfinity>,
    ] extends infer R extends [boolean, boolean, boolean, boolean]
    	? [true, false] extends [R[0], R[3]] // Note: without parentheses
    		? PositiveInfinity
    		: 'failed'
    	: never;
    
    
    type AddTest0 = Add<PositiveInfinity, PositiveInfinity>; // failed
    type AddTest1 = AddWithoutParentheses<PositiveInfinity, PositiveInfinity>; // Infinity

    playground link

    I guess the parentheses make it a complex type?
    and, Isn't IsEqual implemented correctly either? :(

  11. Max10240 commented on Oct 30, 2023

    @Max10240
    Author

    Thank God, I found it works well in this way(with infer)!

    export type AddWithInfer<A extends number, B extends number> = [
    	IsEqual<A, PositiveInfinity>, IsEqual<A, NegativeInfinity>,
    	IsEqual<B, PositiveInfinity>, IsEqual<B, NegativeInfinity>,
    ] extends infer R extends [boolean, boolean, boolean, boolean]
    	? [true, false] extends (infer _ extends [R[0], R[3]]) // Note: with infer
    		? PositiveInfinity
    		: 'failed'
    	: never;
    
    type AddTest2 = AddWithInfer<PositiveInfinity, PositiveInfinity>; // Infinity, works too!
  12. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Working as IntendedThe behavior described is the intended behavior; this is not a bug

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions