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

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
Max10240 opened this issue Oct 7, 2022 · 10 comments
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Max10240
Copy link

Max10240 commented Oct 7, 2022

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)

@DanielRosenwasser
Copy link
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'

@DanielRosenwasser DanielRosenwasser added the Needs Investigation This issue needs a team member to investigate its status. label Oct 7, 2022
@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Oct 7, 2022
@Max10240
Copy link
Author

Max10240 commented Oct 7, 2022

@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

@Andarist
Copy link
Contributor

Andarist commented Oct 7, 2022

@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?

@DanielRosenwasser
Copy link
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.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Oct 7, 2022

I spoke with @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.

@Max10240
Copy link
Author

Max10240 commented Oct 8, 2022

@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.

@ahejlsberg
Copy link
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.

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Bug A bug in TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Oct 8, 2022
@Max10240
Copy link
Author

Max10240 commented Oct 9, 2022

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

@Max10240
Copy link
Author

Max10240 commented Oct 30, 2023

@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? :(

@Max10240
Copy link
Author

Max10240 commented Oct 30, 2023

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!

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