Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
AnyhowStep opened this issue Jul 17, 2019 · 9 comments
Labels
Needs Investigation This issue needs a team member to investigate its status.
Milestone

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 17, 2019

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)


@AnyhowStep
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

AnyhowStep commented Jul 17, 2019

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

AnyhowStep commented Jul 27, 2019

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
Copy link
Contributor Author

AnyhowStep commented Jul 27, 2019

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
Copy link
Member

@AnyhowStep TL;DR ?

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Jul 30, 2019

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
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

3 participants