Skip to content

Functions with same intersection and conditional type in parameter list not assignable to each other #32442

Open

Description

@AnyhowStep

TypeScript Version: 3.5.1

Search Terms:

function, intersection, conditional type, parameter list, assignable

Code

/**
 * We should never be able to create a value of this type legitimately.
 * 
 * `ErrorMessageT` is our error message
 */
interface CompileError<ErrorMessageT extends any[]> {
  /**
   * There should never be a value of this type
   */
  readonly __compileError : never;
}
type ErrorA = CompileError<["I am error A"]>;
type ErrorB = CompileError<["I am error B"]>;

declare const errorA : ErrorA;
/**
 * Different compile errors are assignable to each other.
 */
const errorB : ErrorB = errorA;

/**
 * Pretend this is `v1.0.0` of your library.
 */
declare function foo <N extends number> (
  /**
   * This is how we use `CompileError<>` to prevent `3` from being
   * a parameter
   */
  n : (
    N &
    (Extract<3, N> extends never ?
    unknown :
    CompileError<[3, "is not allowed; received", N]>)
  )
) : void;

/**
 * Argument of type '3' is not assignable to parameter of type
 * 'CompileError<[3, "is not allowed; received", 3]>'.
 */
foo(3);
/**
 * OK!
 */
foo(5);
/**
 * Argument of type '3 | 5' is not assignable to parameter of type
 * 'CompileError<[3, "is not allowed; received", 3 | 5]>'.
 */
foo(5 as 3|5);
/**
 * Argument of type 'number' is not assignable to parameter of type
 * 'CompileError<[3, "is not allowed; received", number]>'.
 */
foo(5 as number);

///////////////////////////////////////////////////////////////////

/**
 * The same as `foo<>()` but with a different error message.
 * 
 * Pretend this is `v1.1.0` of your library.
 */
declare function bar <N extends number> (
  n : (
    N &
    (Extract<3, N> extends never ?
    unknown :
    CompileError<[3, "is not allowed; received", N]>)
  )
) : void;

/**
 * Expected: Assignable to each other
 * Actual: Not assignable to each other
 */
const fooIsAssignableToBar : typeof bar = foo;
const barIsAssignableToFoo : typeof foo = bar;

Expected behavior:

The following should have no errors,

const fooIsAssignableToBar : typeof bar = foo;
const barIsAssignableToFoo : typeof foo = bar;

Actual behavior:

It has errors

Playground Link:

Playground

Related Issues:

#21756

Also, related to my comment here,

#23689 (comment)


Activity

  1. AnyhowStep commented on Jul 17, 2019

    @AnyhowStep
    ContributorAuthor

    If you remove the N & part from the parameters and replace unknown with N, it works.

    /**
     * We should never be able to create a value of this type legitimately.
     * 
     * `ErrorMessageT` is our error message
     */
    interface CompileError<ErrorMessageT extends any[]> {
      /**
       * There should never be a value of this type
       */
      readonly __compileError : never;
    }
    type ErrorA = CompileError<["I am error A"]>;
    type ErrorB = CompileError<["I am error B"]>;
    
    declare const errorA : ErrorA;
    /**
     * Different compile errors are assignable to each other.
     */
    const errorB : ErrorB = errorA;
    
    /**
     * Pretend this is `v1.0.0` of your library.
     */
    declare function foo <N extends number> (
      /**
       * This is how we use `CompileError<>` to prevent `3` from being
       * a parameter
       */
      n : (
        (Extract<3, N> extends never ?
        N :
        CompileError<[3, "is not allowed; received", N]>)
      )
    ) : void;
    
    /**
     * Argument of type '3' is not assignable to parameter of type
     * 'CompileError<[3, "is not allowed; received", 3]>'.
     */
    foo(3);
    /**
     * OK!
     */
    foo(5);
    /**
     * Argument of type '3 | 5' is not assignable to parameter of type
     * 'CompileError<[3, "is not allowed; received", 3 | 5]>'.
     */
    foo(5 as 3|5);
    /**
     * Argument of type 'number' is not assignable to parameter of type
     * 'CompileError<[3, "is not allowed; received", number]>'.
     */
    foo(5 as number);
    
    ///////////////////////////////////////////////////////////////////
    
    /**
     * The same as `foo<>()` but with a different error message.
     * 
     * Pretend this is `v1.1.0` of your library.
     */
    declare function bar <N extends number> (
      n : (
        (Extract<3, N> extends never ?
        N :
        CompileError<[3, "is not allowed; received", N]>)
      )
    ) : void;
    
    /**
     * Expected: Assignable to each other
     * Actual: Assignable to each other
     */
    const fooIsAssignableToBar : typeof bar = foo;
    const barIsAssignableToFoo : typeof foo = bar;

    Playground

  2. AnyhowStep commented on Jul 17, 2019

    @AnyhowStep
    ContributorAuthor

    The motivation behind introducing intersection types is so that multiple compile-time requirements can be chained together,

    declare function bar <N extends number> (
      n : (
        N &
        (Extract<3, N> extends never ? unknown : CompileError<[3, "is not allowed; received", N]>) &
        (Extract<4, N> extends never ? unknown : CompileError<[4, "is not allowed; received", N]>) &
        (SomeOtherComplicatedCondition<N>)
      )
    ) : void;

    Of course, with such a "basic" type like number, the conditions aren't very interesting.


    A code snippet of something more complicated,

    export type AssertNotInPreviousJoinsImpl<
        QueryT extends IQuery,
        AliasedTableT extends IAliasedTable
    > = (
        QueryT["_joins"] extends IJoin[] ?
        (
            Extract<
                AliasedTableT["alias"],
                JoinArrayUtil.TableAliases<QueryT["_joins"]>
            > extends never ?
            unknown :
            CompileError<[
                "Alias",
                Extract<
                    AliasedTableT["alias"],
                    JoinArrayUtil.TableAliases<QueryT["_joins"]>
                >,
                "already used in previous JOINs",
                JoinArrayUtil.TableAliases<QueryT["_joins"]>
            ]>
        ) :
        unknown
    );
    export type AssertNotInParentJoinsImpl<
        QueryT extends IQuery,
        AliasedTableT extends IAliasedTable
    > = (
        QueryT["_parentJoins"] extends IJoin[] ?
        (
            Extract<
                AliasedTableT["alias"],
                JoinArrayUtil.TableAliases<QueryT["_parentJoins"]>
            > extends never ?
            unknown :
            CompileError<[
                "Alias",
                Extract<
                    AliasedTableT["alias"],
                    JoinArrayUtil.TableAliases<QueryT["_parentJoins"]>
                >,
                "already used in parent JOINs",
                JoinArrayUtil.TableAliases<QueryT["_parentJoins"]>
            ]>
        ) :
        unknown
    );
    export type AssertNoUsedRefImpl<
        AliasedTableT extends IAliasedTable
    > = (
        Extract<keyof AliasedTableT["usedRef"], string> extends never ?
        unknown :
        CompileError<[
            "Derived table",
            AliasedTableT["alias"],
            "must not reference outer query tables",
            Writable<
                ColumnIdentifierUtil.FromColumnRef<AliasedTableT["usedRef"]>
            >
        ]>
    );
    export type AssertValidJoinTargetImpl<
        QueryT extends IQuery,
        AliasedTableT extends IAliasedTable
    > = (
        & AssertNotInPreviousJoinsImpl<QueryT, AliasedTableT>
        & AssertNotInParentJoinsImpl<QueryT, AliasedTableT>
        & AssertNoUsedRefImpl<AliasedTableT>
    );
    export type AssertValidJoinTarget<
        QueryT extends IQuery,
        AliasedTableT extends IAliasedTable
    > = (
        AliasedTableT &
        AssertValidJoinTargetImpl<QueryT, AliasedTableT>
    );

    If all the conditions are satisfied, AssertValidJoinTarget<QueryT, AliasedTableT> resolves to AliasedTableT.

    If at least one of the conditions fail, it resolves to AliasedTableT & CompileError<>

  3. AnyhowStep commented on Jul 17, 2019

    @AnyhowStep
    ContributorAuthor

    It seems like if I nest conditional types instead of introducing intersections, it works,

    /**
     * We should never be able to create a value of this type legitimately.
     * 
     * `ErrorMessageT` is our error message
     */
    interface CompileError<ErrorMessageT extends any[]> {
      /**
       * There should never be a value of this type
       */
      readonly __compileError : never;
    }
    type ErrorA = CompileError<["I am error A"]>;
    type ErrorB = CompileError<["I am error B"]>;
    
    declare const errorA : ErrorA;
    /**
     * Different compile errors are assignable to each other.
     */
    const errorB : ErrorB = errorA;
    
    type IfExtractExtendsNever<A, B, TrueT, FalseT> = (
      Extract<A, B> extends never ? TrueT : FalseT
    );
    type Condition0<N, TrueT> = (
      IfExtractExtendsNever<
        3, N,
        TrueT,
        CompileError<[3, "is not allowed; received", N]>
      >
    );
    type Condition1<N, TrueT> = (
      IfExtractExtendsNever<
        4, N,
        TrueT,
        CompileError<[4, "is not allowed; received", N]>
      >
    );
    type Condition2<N, TrueT> = (
      IfExtractExtendsNever<
        3, N,
        TrueT,
        CompileError<[3, "is not allowed; received", N, "Same as Condition0 but with different error message"]>
      >
    );
    type Condition3<N, TrueT> = (
      IfExtractExtendsNever<
        4, N,
        TrueT,
        CompileError<[4, "is not allowed; received", N, "Same as Condition1 but with different error message"]>
      >
    );
    /**
     * Pretend this is `v1.0.0` of your library.
     */
    declare function foo <N extends number> (
      /**
       * This is how we use `CompileError<>` to prevent `3` from being
       * a parameter
       */
      n : (
        Condition0<N, Condition1<N, N>>
      )
    ) : void;
    
    /**
     * Argument of type '3' is not assignable to parameter of type
     * 'CompileError<[3, "is not allowed; received", 3]>'.
     */
    foo(3);
    /**
     * OK!
     */
    foo(5);
    /**
     * Argument of type '3 | 5' is not assignable to parameter of type
     * 'CompileError<[3, "is not allowed; received", 3 | 5]>'.
     */
    foo(5 as 3|5);
    /**
     * Argument of type 'number' is not assignable to parameter of type
     * 'CompileError<[3, "is not allowed; received", number]>'.
     */
    foo(5 as number);
    
    ///////////////////////////////////////////////////////////////////
    
    /**
     * The same as `foo<>()` but with a different error message.
     * 
     * Pretend this is `v1.1.0` of your library.
     */
    declare function bar <N extends number> (
      n : (
        Condition2<N, Condition3<N, N>>
      )
    ) : void;
    
    /**
     * Expected: Assignable to each other
     * Actual: Assignable to each other
     */
    const fooIsAssignableToBar : typeof bar = foo;
    const barIsAssignableToFoo : typeof foo = bar;

    Playground

  4. AnyhowStep commented on Jul 27, 2019

    @AnyhowStep
    ContributorAuthor

    This assignability situation is actually making life kind of hard ._.

    /**
     * We should never be able to create a value of this type legitimately.
     * 
     * `ErrorMessageT` is our error message
     */
    interface CompileError<ErrorMessageT extends any[]> {
      /**
       * There should never be a value of this type
       */
      readonly __compileError : never;
    }
    type ErrorA = CompileError<["I am error A"]>;
    type ErrorB = CompileError<["I am error B"]>;
    
    declare const errorA : ErrorA;
    /**
     * Different compile errors are assignable to each other.
     */
    const errorB : ErrorB = errorA;
    
    /**
     * Expected:
     * OK!
     * 
     * Actual:
     * Type
     * '<N extends number>(n: N & (Extract<3, N> extends never ? unknown : CompileError<[3, "is not allowed; received", N]>)) => void'
     * is not assignable to type
     * '<N extends number>(n: N & (Extract<3, N> extends never ? unknown : CompileError<[3, "is not allowed; received", N]>)) => void'.
     * Two different types with this name exist, but they are unrelated.
     */
    const blah : (
      <N extends number> (
        n : (
          N &
          (Extract<3, N> extends never ?
          unknown :
          CompileError<[3, "is not allowed; received", N]>)
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          (Extract<3, N> extends never ?
          unknown :
          CompileError<[3, "is not allowed; received", N]>)
        )
      ) : void => {
    
      }
    )

    Playground


    In the above, you see something like A is not assignable to A. And that just confuses me.

    My actual use-case has to do with having function properties (not methods) on a generic class.

    And the function uses the this keyword.

  5. AnyhowStep commented on Jul 27, 2019

    @AnyhowStep
    ContributorAuthor

    Smaller repro,

    /**
     * Expected:
     * OK!
     * 
     * Actual:
     * Type
     * '<N extends number>(n: N & (3 extends N ? any : any)) => void'
     * is not assignable to type
     * '<N extends number>(n: N & (3 extends N ? any : any)) => void'.
     * Two different types with this name exist, but they are unrelated.
     */
    const blah : (
      <N extends number> (
        n : (
          N &
          (3 extends N ? any : any)
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          (3 extends N ? any : any)
        )
      ) : void => {
    
      }
    )

    Playground

  6. AnyhowStep commented on Jul 27, 2019

    @AnyhowStep
    ContributorAuthor
    type BLAH<N> = 3 extends N ? any : any;
    /**
     * Expected:
     * OK!
     * 
     * Actual:
     * Types of parameters 'n' and 'n' are incompatible.
     * Type 'N & BLAH<N>' is not assignable to type 'N & BLAH<N> & BLAH<N & BLAH<N>>'.
     * Type 'N & BLAH<N>' is not assignable to type 'BLAH<N & BLAH<N>>'.
     */
    const blah : (
      <N extends number> (
        n : (
          N &
          BLAH<N>
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          BLAH<N>
        )
      ) : void => {
    
      }
    )

    Playground


    Even a type assertion does not work,

    type BLAH<N> = 3 extends N ? any : any;
    /**
     * Expected:
     * OK!
     * 
     * Actual:
     * Types of parameters 'n' and 'n' are incompatible.
     * Type 'N & BLAH<N>' is not assignable to type 'N & BLAH<N> & BLAH<N & BLAH<N>>'.
     * Type 'N & BLAH<N>' is not assignable to type 'BLAH<N & BLAH<N>>'.
     */
    const blah : (
      <N extends number> (
        n : (
          N &
          BLAH<N>
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          BLAH<N>
        )
      ) : void => {
    
      }
    ) as (
      <N extends number> (
        n : (
          N &
          BLAH<N>
        )
      ) => void
    )

    Playground

  7. AnyhowStep commented on Jul 27, 2019

    @AnyhowStep
    ContributorAuthor

    So, if I put the generic type param on the RHS of the extends, it is not assignable,

    //Error, cannot assign
    const blah : (
      <N extends number> (
        n : (
          N &
          (any extends N ? any : any)
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          (any extends N ? any : any)
        )
      ) : void => {
    
      }
    )

    Playground


    If I put it on the LHS of the extends, it is assignable,

    //OK! Can assign
    const blah : (
      <N extends number> (
        n : (
          N &
          (N extends 3 ? unknown : ["blah"])
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          (N extends 3 ? unknown : ["blah"])
        )
      ) : void => {
    
      }
    )

    Playground


    And I have zero intuition for why this is the case.

  8. RyanCavanaugh commented on Jul 30, 2019

    @RyanCavanaugh
    Member

    AnyhowStep TL;DR ?

  9. AnyhowStep commented on Jul 30, 2019

    @AnyhowStep
    ContributorAuthor

    Umm... Kind of hard to make a TL;DR.


    TL;DR

    I have a generic function.
    It has a parameter.
    That parameter uses a conditional type and intersection type (at the same time).

    Because of the conditional and intersection type, a different function with the exact same signature is treated as a different type... Even though they are the same type. A type assertion does not work, either.

    I don't know if it is a bug, design limitation, or by design.

    And I don't have an intuition for why it thinks it is assignable in some cases, but not assginable in other cases.


    //Error, cannot assign
    const blah : (
      <N extends number> (
        n : (
          N &
          (any extends N ? any : any)
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          (any extends N ? any : any)
        )
      ) : void => {
    
      }
    )

    Playground

    Playground showing type assertion not working

    Notice that the type annotation and the type of the function have the same "type". But the compiler thinks they're different types.


    But I have found that this is not always the case. In certain cases, the compiler knows they're the same type.

    Like when I put N on the LHS of the extends.

    //OK! Can assign
    const blah : (
      <N extends number> (
        n : (
          N &
          (N extends 3 ? unknown : ["blah"])
        )
      ) => void
    ) = (
      <N extends number> (
        n : (
          N &
          (N extends 3 ? unknown : ["blah"])
        )
      ) : void => {
    
      }
    )

    Playground

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs InvestigationThis issue needs a team member to investigate its status.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions