question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Return type inference breaks in function parameter; tooltip also inconsistent

See original GitHub issue

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,

https://github.com/microsoft/TypeScript/issues/29133

https://github.com/microsoft/TypeScript/issues/23689#issuecomment-512114782

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()

Issue Analytics

  • State:open
  • Created 4 years ago
  • Comments:17 (17 by maintainers)

github_iconTop GitHub Comments

1reaction
RyanCavanaughcommented, Jul 25, 2019

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

0reactions
AnyhowStepcommented, Aug 11, 2019

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, https://github.com/microsoft/TypeScript/issues/14829#issuecomment-520191642

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why does parameter type inference break in compose function?
If we inspect the inferred types of the parameters of the functions that are passed, we see that nPlus100 is correctly inferred to...
Read more >
Linter rules - Dart
When declaring a method or function always specify a return type. Declaring return types for functions helps improve your codebase by allowing the...
Read more >
PHP Tools for Visual Studio and VS Code by DEVSENSE
We have fixed the control flow analysis, when there are functions that never return (i.e. they throw an exception instead of returning a...
Read more >
Parameter Info in a Legacy Language Service2 - Visual Studio ...
IntelliSense Parameter Info is a tooltip that displays the signature of a method when the user types the parameter list start character ...
Read more >
Template argument deduction - cppreference.com
For auto-returning functions, the parameter P is obtained as follows: in T , the declared return type of the function that includes auto...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found