Skip to content

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

Open
@AnyhowStep

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

AnyhowStep

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

AnyhowStep

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

AnyhowStep

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

AnyhowStep

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.

AnyhowStep

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

AnyhowStep

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

AnyhowStep

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.

RyanCavanaugh

RyanCavanaugh commented on Jul 30, 2019

@RyanCavanaugh
Member

@AnyhowStep TL;DR ?

AnyhowStep

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

        Participants

        @weswigham@AnyhowStep@RyanCavanaugh

        Issue actions

          Functions with same intersection and conditional type in parameter list not assignable to each other · Issue #32442 · microsoft/TypeScript