TypeScript: Return type inference breaks in function parameter; tooltip also inconsistent

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

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 18 (18 by maintainers)

Most upvoted comments

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