Skip to content

Return type inference breaks in function parameter; tooltip also inconsistent #32540

Closed

Description

@AnyhowStep

TypeScript Version: 3.5.1

Search Terms:

conditional type, type arg, inference, return type, argument, parameter, generic, callback function

Code

This is literally the minimum repro I could make and it's still too big.

export interface CompileError<_ErrorMessageT extends any[]> {
    /**
     * There should never be a value of this type
     */
    readonly __compileError : never;
}
/**
 * Each `string` element represents a column name.
 *
 * A "key" is a set of columns that uniquely identifies
 * a row in a table.
 */
export type Key = readonly string[];

export type ExtractSubKey<
    A extends Key,
    B extends Key
> = (
    A extends Key ?
    (
        B extends Key ?
        (
            A[number] extends B[number] ?
            A :
            never
        ) :
        never
    ) :
    never
);

export type ExtractSuperKey<
    A extends Key,
    B extends Key
> = (
    A extends Key ?
    (
        B extends Key ?
        (
            B[number] extends A[number] ?
            A :
            never
        ) :
        never
    ) :
    never
);

export type FindSubKey<
    ArrT extends readonly Key[],
    KeyT extends Key
> = (
    ExtractSubKey<
        ArrT[number],
        KeyT
    >
);
export type FindSuperKey<
    ArrT extends readonly Key[],
    KeyT extends Key
> = (
    ExtractSuperKey<
        ArrT[number],
        KeyT
    >
);

declare function noSubKey<KeyT extends Key> (
  arg : (
    (c : {
      x : "x",
      y : "y",
      z : "z"
    }) => KeyT &
    (
        FindSuperKey<
            (("x"|"y")[])[],
            KeyT
        > extends never ?
        unknown :
        CompileError<[
            KeyT,
            "is a sub key of",
            FindSuperKey<
                (("x"|"y")[])[],
                KeyT
            >
        ]>
    )
  )
) : void

//OK!
//Expected: Infer KeyT as "z"[]
//Actual  : Infer KeyT as "z"[]
//Tooltip : noSubKey<"z"[]>
noSubKey(c => [c.z])
//OK!
//Expected: CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Actual  : CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Error!
//Tooltip : noSubKey<readonly string[]>
noSubKey(c => [c.x])
//OK!
//Expected: CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Actual  : CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Error!
//Tooltip : noSubKey<readonly string[]>
noSubKey(c => [c.x, c.y])

//OK!
//Expected: Infer KeyT as "z"[]
//Actual  : Infer KeyT as "z"[]
//Tooltip : noSubKey<"z"[]>
noSubKey(() => ["z" as "z"]);
//OK!
//Expected: CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Actual  : CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Tooltip : noSubKey<"x"[]>
noSubKey(() => ["x" as "x"]);
//OK!
//Expected: CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Actual  : CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Tooltip : noSubKey<("x" | "y")[]>
noSubKey(() => ["x" as "x", "y" as "y"]);

declare function noSuperKey<KeyT extends Key> (
  arg : (
    ((c : {
      x : "x",
      y : "y",
      z : "z"
    }) => KeyT) &
    (
        FindSubKey<
            (("x"|"y")[])[],
            KeyT
        > extends never ?
        unknown :
        CompileError<[
            KeyT,
            "is a super key of",
            FindSubKey<
                (("x"|"y")[])[],
                KeyT
            >
        ]>
    )
  )
) : void

//Error!
//Expected: Infer KeyT as "z"[]
//Actual  : CompileError<[readonly string[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<readonly string[]>
noSuperKey(c => [c.z])
//Error!
//Expected: Infer KeyT as "x"[]
//Actual  : CompileError<[readonly string[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<readonly string[]>
noSuperKey(c => [c.x])
//Error!
//Expected: CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
//Actual  : CompileError<[readonly string[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<readonly string[]>
noSuperKey(c => [c.x, c.y])

//OK!
//Expected: Infer KeyT as "z"[]
//Actual  : Infer KeyT as "z"[]
//Tooltip : noSuperKey<"z"[]>
noSuperKey(() => ["z" as "z"]);
//OK!
//Expected: Infer KeyT as "x"[]
//Actual  : Infer KeyT as "x"[]
//Tooltip : noSuperKey<"x"[]>
noSuperKey(() => ["x" as "x"]);
//OK!
//Expected: CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
//Actual  : CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<("x" | "y")[]>
noSuperKey(() => ["x" as "x", "y" as "y"]);

/**
 * Seems weird that using the `c` argument results in inference problems.
 * But using string literals without the `c` argument is okay.
 */

Expected behavior:

Whether I use the c argument or not when calling noSubKey()/noSuperKey(),
it should always infer the type of KeyT correctly for all cases.

Actual behavior:

noSubKey()

  • With c argument, it correctly infers KeyT in the error message

  • With c argument, it incorrectly infers KeyT in the tooltip

  • With string literals, it correctly infers KeyT in the error message

  • With string literals, it correctly infers KeyT in the tooltip

noSuperKey()

  • With c argument, it incorrectly infers KeyT in the error message

  • With c argument, it incorrectly infers KeyT in the tooltip

  • With string literals, it correctly infers KeyT in the error message

  • With string literals, it correctly infers KeyT in the tooltip

Playground Link:

Playground

Related Issues:

Off the top of my head, I've made similar reports before and other similar repro cases,

#29133

#23689 (comment)

From my personal experience, it feels like the moment you are using the ReturnType<> of a function in a conditional type (directly or indirectly) inside a parameter, you end up having weird issues with inference.

For the longest time, I've been trying to find a solid workaround but nothing seems to quite stick.

Every workaround I can come up with will work in some situation but not in others.


You'll notice that, in this repro, I never even use ReturnType<> on FunctionT.

I use KeyT and make the parameter (c) => KeyT and it works in some cases but breaks in this case for noSuperKey() and works (mostly) fine for noSubKey()

Activity

  1. AnyhowStep commented on Jul 24, 2019

    @AnyhowStep
    ContributorAuthor

    What's extra weird to me is that c.x is literally (pun, ha) of the literal type "x".

    But using "x" as "x" is somehow more "kosher".

  2. AnyhowStep commented on Jul 24, 2019

    @AnyhowStep
    ContributorAuthor

    Also, if you move c outside of the function params... It works fine again,

    export interface CompileError<_ErrorMessageT extends any[]> {
        /**
         * There should never be a value of this type
         */
        readonly __compileError : never;
    }
    /**
     * Each `string` element represents a column name.
     *
     * A "key" is a set of columns that uniquely identifies
     * a row in a table.
     */
    export type Key = readonly string[];
    
    export type ExtractSubKey<
        A extends Key,
        B extends Key
    > = (
        A extends Key ?
        (
            B extends Key ?
            (
                A[number] extends B[number] ?
                A :
                never
            ) :
            never
        ) :
        never
    );
    
    export type FindSubKey<
        ArrT extends readonly Key[],
        KeyT extends Key
    > = (
        ExtractSubKey<
            ArrT[number],
            KeyT
        >
    );
    
    /**
     * Moved `c` to a global variable.
     * `c` is no longer a function arg.
     */
    declare const c : {
      x : "x",
      y : "y",
      z : "z"
    };
    declare function noSuperKey<KeyT extends Key> (
      arg : (
        (() => KeyT) &
        (
            FindSubKey<
                (("x"|"y")[])[],
                KeyT
            > extends never ?
            unknown :
            CompileError<[
                KeyT,
                "is a super key of",
                FindSubKey<
                    (("x"|"y")[])[],
                    KeyT
                >
            ]>
        )
      )
    ) : void
    
    //OK!
    //Expected: Infer KeyT as "z"[]
    //Actual  : Infer KeyT as "z"[]
    //Tooltip : noSubKey<"z"[]>
    noSuperKey(() => [c.z])
    //OK!
    //Expected: Infer KeyT as "x"[]
    //Actual  : Infer KeyT as "x"[]
    //Tooltip : noSubKey<"x"[]>
    noSuperKey(() => [c.x])
    //OK!
    //Expected: CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
    //Actual  : CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
    //Tooltip : noSubKey<("x" | "y")[]>
    noSuperKey(() => [c.x, c.y])
    
    //OK!
    //Expected: Infer KeyT as "z"[]
    //Actual  : Infer KeyT as "z"[]
    //Tooltip : noSubKey<"z"[]>
    noSuperKey(() => ["z" as "z"]);
    //OK!
    //Expected: Infer KeyT as "x"[]
    //Actual  : Infer KeyT as "x"[]
    //Tooltip : noSubKey<"x"[]>
    noSuperKey(() => ["x" as "x"]);
    //OK!
    //Expected: CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
    //Actual  : CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
    //Tooltip : noSubKey<("x" | "y")[]>
    noSuperKey(() => ["x" as "x", "y" as "y"]);

    Playground

  3. AnyhowStep commented on Jul 24, 2019

    @AnyhowStep
    ContributorAuthor

    So, "x" as "x" and global c.x is okay with inference.

    Function arg c.x is bad for inference.

  4. AnyhowStep commented on Jul 24, 2019

    @AnyhowStep
    ContributorAuthor

    In my particular use case, I'm writing a fluent API for a builder. Each method returns a new instance of the builder.

    I'm using callback functions and checking the return type because there are some complicated constraints I need to enforce during compile time

  5. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    Changing the types to,

    export type ExtractSubKey<
        A extends Key,
        B extends Key
    > = (
        A[number] extends B[number] ?
        A :
        never
    );
    
    export type ExtractSuperKey<
        A extends Key,
        B extends Key
    > = (
        B[number] extends A[number] ?
        A :
        never
    );

    does not affect the results.

    So, smaller repro,

    Playground

  6. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    It seems like moving the conditional type to the this param doesn't work either,

    declare function noSuperKey<KeyT extends Key> (
      this : (
        FindSubKey<
            (("x"|"y")[])[],
            KeyT
        > extends never ?
        any :
        CompileError<[
            KeyT,
            "is a super key of",
            FindSubKey<
                (("x"|"y")[])[],
                KeyT
            >
        ]>
      ),
      arg : (
        ((c : {
          x : "x",
          y : "y",
          z : "z"
        }) => KeyT)
      )
    ) : void

    Still works for noSubKey(), though

    Playground


    If you hover over the inferred type of the callback functions, it seems to "know" the correct type.

    image

    It's just that the conditional type inside the param and the type param gets all confused.

    Adding an explicit cast to the type that TS already knows seems to "fix" the inference.

    //OK!
    //Expected: Infer KeyT as "z"[]
    //Actual  : Infer KeyT as "z"[]
    //Tooltip : noSuperKey<"z"[]>
    //Inferred type of callback: (c: { x: "x", y: "y", z: "z" }) => "z"[]
    noSuperKey(
      (c => [c.z]) as (
        //Add an explicit cast and it works
        (c: { x: "x", y: "y", z: "z" }) => "z"[]
      )
    );

    Playground

  7. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    It looks like the order of A and B matters in ExtractSubKey<> and ExtractSuperKey<>.

    They are almost exactly the same; it's just that one is A[number] extends B[number], the other is B[number] extends A[number]

    This gives problems,

    A[number] extends B[number] ?

    This does not, (not as much)

    B[number] extends A[number] ?
  8. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    Uhhhh..... Wat?

    Changing the implementation of ExtractSubKey<> and ExtractSuperKey<> to this makes both work (reasonably) well,

    //This works, I have no idea why.
    export type ExtractSubKey<
        A extends Key,
        B extends Key
    > = (
        A[number] extends Extract<B[number], A[number]> ?
        A :
        never
    );
    
    export type ExtractSuperKey<
        A extends Key,
        B extends Key
    > = (
        B[number] extends Extract<A[number], B[number]> ?
        A :
        never
    );

    I think I'm going crazy because all I did was shift types around (out of desperation, lol) and it works now... And I have no idea why one implementation works better than the other.

    Also, this implementation is waywayway more complicated than the original implementation.... But somehow works.


    The definition of insanity is doing the same thing over and over again, but expecting different results.
    -Not Albert Einstein but many people say he said it anyway

    I only tried this convoluted implementation because I wanted to do something different.
    I wanted to stir the primordial soup of types and see if anything would evolve and survive the harsh world of the TS inference mechanism.

    I give this implementation my blessing. May it go forth and procreate.
    It is the fittest of the implementations and may live in my codebase.


    There are still problems with the tooltip and it concerns me.

    //OK!
    //Expected: CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
    //Actual  : CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
    //Error!
    //Tooltip : noSubKey<readonly string[]> <-- Expected ("x" | "y")[]
    //Error!
    //this    : any <-- Expected CompileError<>
    noSubKey(c => [c.x, c.y])

    image


    Full repro and details comments about what is expected and what actually happens,

    Playground

  9. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    I re-introduced the conditional types in ExtractSubKey<> and ExtractSuperKey<> to distribute the types of A and B and it still works (apart from tooltips generating the wrong types in some cases),

    export type ExtractSubKey<
        A extends Key,
        B extends Key
    > = (
      A extends Key ?
      (
        B extends Key ?
        (
          A[number] extends Extract<B[number], A[number]> ?
          A :
          never
        ) :
        never
      ) :
      never
    );
    
    export type ExtractSuperKey<
        A extends Key,
        B extends Key
    > = (
      A extends Key ?
      (
        B extends Key ?
        (
          B[number] extends Extract<A[number], B[number]> ?
          A :
          never
        ) :
        never
      ) :
      never
    );

    Playground


    With this implementation,

    noSubKey()

    • With c argument, it correctly infers KeyT in the error message

    • With c argument, it incorrectly infers KeyT in the tooltip

    • With string literals, it correctly infers KeyT in the error message

    • With string literals, it correctly infers KeyT in the tooltip

    noSuperKey()

    • With c argument, it correctly infers KeyT in the error message

    • With c argument, it incorrectly infers KeyT in the tooltip

    • With string literals, it correctly infers KeyT in the error message

    • With string literals, it correctly infers KeyT in the tooltip

  10. changed the title [-]Return type inference breaks in function parameter[/-] [+]Return type inference breaks in function parameter; tooltip also inconsistent[/+] on Jul 25, 2019
  11. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    I tried changing,

    • () => KeyT
    • KeyT extends Key

    to,

    • D extends SomeDelegate
    • ReturnType<D>

    It gives the same result as KeyT,
    Playground


    This is super weird to me because I have been under the impression that ReturnType<> is broken when used in function parameters for the longest time. I've even filed issues about it and mentioned it in comments elsewhere with examples of how it seems to be broken.

    However, in my library, I could find examples of it working in some cases and not working in other cases. They were all such complicated examples that I could never find a small repro.

    This is the first time I've ever actually found a decently small repro and found evidence that it isn't a ReturnType<DelegateT> vs. ReturnT problem. It's just that some generic types aren't inference-friendly.


    As an anecdote, I had many attempts at (relatively) simple things using ReturnType<DelegateT> in function params and they failed really bad.

    However, there was one single attempt that worked.

    It was the type-level equivalent of,

    for (let i=0; i<ArrT["length"]; ++i) {
        for (let j=i+1; j<ArrT["length"]; ++j) {
            /*snip complicated type using ArrT, i and j*/
        }
    }

    A type-level nested for-loop over a generic array type! And the actual type was an intersection of at least four other more complicated types.

    I wanted to be consistent with the rest of my codebase and refactor it to not use ReturnType<DelegateT>. However, it was so complex and scary and worked that I didn't dare to touch it.

    So, while everything else abandoned ReturnType<DelegateT> in function params, that one function continued to have it. It was the only thing that defied my intuition that ReturnType<DelegateT> in param = bad

  12. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    I needed a version that was subKeyOnly() and superKeyOnly().

    I tried a modified version what I posted above and... It doesn't work.

    //THIS IS BROKEN
    declare function subKeyOnly<KeyT extends Key> (
      this : (
        FindSuperKey<
            (("x"|"y")[])[],
            KeyT
        > extends never ?
        CompileError<[
            KeyT,
            "is NOT a sub key of",
            (("x"|"y")[])[]
        ]> :
        any
      ),
      arg : (
        (c : {
          x : "x",
          y : "y",
          z : "z"
        }) => KeyT 
      )
    ) : void
    //THIS IS BROKEN
    declare function superKeyOnly<KeyT extends Key> (
      this : (
        FindSubKey<
            (("x"|"y")[])[],
            KeyT
        > extends never ?
        CompileError<[
            KeyT,
            "is NOT a super key of",
            (("x"|"y")[])[]
        ]> :
        any
      ),
      arg : (
        ((c : {
          x : "x",
          y : "y",
          z : "z"
        }) => KeyT)
      )
    ) : void

    Playground

    As usual, string literals are okay, but using function args breaks.


    While the inferred type of KeyT is broken by simply switching the types in the true and false branches of the conditional type, it seems that the tooltip display for the type of this is consistent, at least.

    It shows CompileError<> instead of any

  13. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    By reverting back to this,

    export type ExtractSubKey<
        A extends Key,
        B extends Key
    > = (
        A[number] extends B[number] ?
        A :
        never
    );
    
    export type ExtractSuperKey<
        A extends Key,
        B extends Key
    > = (
        B[number] extends A[number] ?
        A :
        never
    );

    I get,

    //Broken
    subKeyOnly(c => [c.z])
    //Broken
    subKeyOnly(c => [c.x])
    //Broken
    subKeyOnly(c => [c.x, c.y])
    
    //OK!
    subKeyOnly(() => ["z" as "z"]);
    //OK!
    subKeyOnly(() => ["x" as "x"]);
    //OK!
    subKeyOnly(() => ["x" as "x", "y" as "y"]);
    
    
    //OK!
    superKeyOnly(c => [c.z])
    //OK!
    superKeyOnly(c => [c.x])
    //OK!
    superKeyOnly(c => [c.x, c.y])
    
    //OK!
    superKeyOnly(() => ["z" as "z"]);
    //OK!
    superKeyOnly(() => ["x" as "x"]);
    //OK!
    superKeyOnly(() => ["x" as "x", "y" as "y"]);

    Playground


    Guess I gotta' figure out how to make it work for this particular case.

  14. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    It seems like this implementation of subKeyOnly() works,

    declare function subKeyOnly<KeyT extends Key> (
      arg : (
        (c : {
          x : "x",
          y : "y",
          z : "z"
        }) => KeyT &
        (
            FindSuperKey<
                (("x"|"y")[])[],
                KeyT
            > extends never ?
            CompileError<[
                KeyT,
                "is not a sub key of",
                (("x"|"y")[])[]
            ]> :
            unknown
        )
      )
    ) : void

    Playground


    If I tweak it ever so slightly, it breaks,
    (Can you spot the difference?)

    Spoiler! The first snippet is,

    (c : {/*snip*/}) => KeyT & (/*snip*/)

    The second snippet is,

    ((c : {/*snip*/}) => KeyT) & (/*snip*/)

    The difference is an extra parentheses.

    declare function subKeyOnly<KeyT extends Key> (
      arg : (
        ((c : {
          x : "x",
          y : "y",
          z : "z"
        }) => KeyT) &
        (
            FindSuperKey<
                (("x"|"y")[])[],
                KeyT
            > extends never ?
            CompileError<[
                KeyT,
                "is not a sub key of",
                (("x"|"y")[])[]
            ]> :
            unknown
        )
      )
    ) : void

    Playground

  15. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    However, moving it to the this param breaks it,

    declare function subKeyOnly<KeyT extends Key> (
      this : (
        FindSuperKey<
            (("x"|"y")[])[],
            KeyT
        > extends never ?
        CompileError<[
            KeyT,
            "is not a sub key of",
            (("x"|"y")[])[]
        ]> :
        unknown
      ),
      arg : (
        (c : {
          x : "x",
          y : "y",
          z : "z"
        }) => KeyT
      )
    ) : void

    Playground


    Moving the callback's type to the inside of the conditional type breaks it,

    declare function subKeyOnly<KeyT extends Key> (
      arg : (
        (
            FindSuperKey<
                (("x"|"y")[])[],
                KeyT
            > extends never ?
            CompileError<[
                KeyT,
                "is not a sub key of",
                (("x"|"y")[])[]
            ]> :
            (c : {
                x : "x",
                y : "y",
                z : "z"
            }) => KeyT
        )
      )
    ) : void

    Playground

  16. RyanCavanaugh commented on Jul 25, 2019

    @RyanCavanaugh
    Member

    AnyhowStep Please unassign yourself when you reach a conclusion and provide a more actionable summary if possible - thanks! 😁

  17. AnyhowStep commented on Jul 25, 2019

    @AnyhowStep
    ContributorAuthor

    Sorry for the wall of comments. I used it to track the myriad of different ways to do this one specific thing,

    type PerformComplicatedCompileTimeChecks<FunctionT extends (...args:any[]) => any, T0, T1, /*snip*/> = (
      /*snip*/ extends /*snip*/ ?
      //success
      FunctionT : 
      //failure
      CompileError<["Some error message"]>
    );
    declare function foo<FunctionT extends (...args:any[]) => any> (
      f : PerformComplicatedCompileTimeChecks<FunctionT, T0, T1, /*snip*/>
    ) : void
    
    //Assume this is OK
    //Return type of `1` is fine
    //Compiles fine.
    //No squiggly
    foo(() => 1);
    
    //Assume this somehow breaks some custom compile-time constraint
    //Return type of `2` violates the very basic principles of the universe
    //Should not compile.
    //Red squiggly saying, CompileError<["Some error message"]>
    foo(() => 2);

    There are many ways to implement the above. We basically need to test the ReturnType<> of FunctionT inside of foo's parameter list.

    • If ReturnType<> satisfies a compile-time check, we let f be FunctionT
    • If ReturnType<> does not satisfy a compile-time check, we let f be not FunctionT

    However, I have not been able to find the one-true-way to implement the above.

    It seems like, no matter what attempt I use, it will work for some cases but completely break for other cases. And it is never intuitive why it works/does not work.

    Getting something like the above to work has involved a lot of trial and error.


    Generally, my attempts break when FunctionT accepts arguments and the concrete type of FunctionT uses those arguments.

    I don't know why, though.

    This has also been noted in a similar issue I created before, and I never found out why.

    Using variables from outside FunctionT or using literals works most of the time.


    For all the cases above where I say "this breaks", it seems to me like they are bugs.

    Because they're all expressing the same thing I want, just with types shuffled around in hopes of appeasing TS' return type inference =x


    I think I've finally found my Neo. I have found The One.
    #32540 (comment)

    I believe the one-true-way for it to work is this,

    type PerformComplicatedCompileTimeChecks<ReturnT, T0, T1, /*snip*/> = (
      ReturnT &
      (
        /*snip*/ extends /*snip*/ ?
        //success
        unknown : 
        //failure
        CompileError<["Some error message"]>
      )
    );
    declare function foo<ReturnT> (
      f : (/*snip args*/) => PerformComplicatedCompileTimeChecks<
        ReturnT, T0, T1, /*snip*/
      >
    ) : void
    
    //Assume this is OK
    //Return type of `1` is fine
    //Compiles fine.
    //No squiggly
    foo(() => 1);
    
    //Assume this somehow breaks some custom compile-time constraint
    //Return type of `2` violates the very basic principles of the universe
    //Should not compile.
    //Red squiggly saying, 2 is not assignable to `2 & CompileError<["Some error message"]>`
    foo(() => 2);
    • If ReturnT satisfies a compile-time check, we let the return type be ReturnT & unknown = ReturnT
    • If ReturnT does not satisfy a compile-time check, we let the return type be ReturnT & CompileError

    However, I will need to test more with my code base.

    When he is ready, I shall take him to see you (The Oracle)


    And if he is The One, I'd like to know how he came to be The One

    But I would like to know if all the other failures are bugs, or design limitations, or by design.

  18. AnyhowStep commented on Aug 11, 2019

    @AnyhowStep
    ContributorAuthor

    After further experimentation, it seems like even my one-true-way attempt isn't perfect.
    It works in most cases.
    Except when you need to use the return type to create another type.

    I'm going to call this other type a correlated type. Because it reminds me of a correlated subquery from SQL.


    This comment shows that the supposed one-true-way (using ReturnT) fails when used with a correlated type,
    #14829 (comment)

  19. RyanCavanaugh commented on Feb 24, 2024

    @RyanCavanaugh
    Member

    Please log a new issue if any action is required on our side. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Needs More InfoThe issue still hasn't been fully clarified

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions