Skip to content

Definitely assignable relation should consider some type variable constraints #25883

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

Closed
3 of 4 tasks
mattmccutchen opened this issue Jul 23, 2018 · 4 comments
Closed
3 of 4 tasks
Labels
Duplicate An existing issue was already created

Comments

@mattmccutchen
Copy link
Contributor

mattmccutchen commented Jul 23, 2018

Search Terms

definitely assignable conditional type variable constraint

Suggestion

The "definitely assignable" relation used to test the condition of a conditional type currently doesn't consider any type variable constraints. checker.ts gives the motivating example:

Without the definitely assignable relation, the type
type Foo<T extends { x: any }> = T extends { x: string } ? string : number
would immediately resolve to 'string' instead of being deferred.

I assume the concern is that immediately resolving to the true branch would be incorrect if T is later set to {x: number}. The problem arises from the nontransitivity of assignability; any is the most obvious source of nontransitivity, but there are some others, such as optional properties. Given type Foo<T extends A> = [T] extends [B] ? C : D, in many cases in which A extends B (at least when A = B), it should be possible to reason that every T that extends A also extends B, or at least implement something that is good enough by TypeScript standards of soundness.

Use Case 1

When using a library in the spirit of https://github.com/gcanti/fp-ts/ that defines existential types and we want an existential of a generic type whose type variable is constrained. Came up in real code here; I was unable to use the library and had to write an existential type by hand for the purpose. Minimized version:

// Type functions and existential types library

const INVARIANT_MARKER = Symbol();
type Invariant<T> = {
  [INVARIANT_MARKER](t: T): T
};

interface TypeFuncs<C, X> {}

const FUN_MARKER = Symbol();
type Fun<K extends keyof TypeFuncs<{}, {}>, C> = Invariant<[typeof FUN_MARKER, K, C]>;

const BAD_APP_MARKER = Symbol();
type BadApp<F, X> = Invariant<[typeof BAD_APP_MARKER, F, X]>;
type App<F, X> = [F] extends [Fun<infer K, infer C>] ? TypeFuncs<C, X>[K] : BadApp<F, X>;

const EX_MARKER = Symbol();
type Ex<F, B> = Invariant<[typeof EX_MARKER, F, B]>;
function makeEx<F, B, X extends B>(x: App<F, X>) {
  return <Ex<F, B>><{}>x;
}
function enterEx<F, B, R>(e: Ex<F, B>, f: <X extends B>(x: App<F, X>) => R) {
  // tslint:disable-next-line:no-any
  return f(<App<F, any>><{}>e);
}

// Example code

enum Kind { FOO, BAR };
const KIND_BRAND = Symbol();
type Name<K> = {name: string} & {[KIND_BRAND]: Invariant<K>};
function brandName<K extends Kind>(str: string) {
  return <Name<K>>{name: str};
}

const F_Name = Symbol();
type F_NAME = Fun<typeof F_Name, never>;
interface TypeFuncs<C, X> {
  [F_Name]: X extends Kind ? Name<X> : never;
};

type ExistentialName = Ex<F_NAME, Kind>;
function makeExistentialName<K extends Kind>(foo: Name<K>): ExistentialName {
  // Error: "Argument of type 'Name<K>' is not assignable to parameter of type 'K extends Kind ? Name<K> : never'."
  // Would be allowed.
  return makeEx<F_NAME, Kind, K>(foo);
}

Use Case 2 (obsolete if #25879 is implemented)

When using the "generic index" workaround described in #25879, I currently have to weaken the constraint of any type variable that needs to accept a generic index, e.g., in type SpanId<A extends AxisL>. I'd like to avoid this by using a conditional type, but I'm foiled by the definitely assignable relation not considering the constraint:

const FAKE_INDEX = "fake-index";
type GenericIndex<_, K> = K | (_ & typeof FAKE_INDEX);

enum Axis {
  ROW = "row",
  COL = "col",
}
type AxisG<_> = GenericIndex<_, Axis>;
const AXIS_BRAND = Symbol();
type SpanId<A extends Axis> = string & {[AXIS_BRAND]: A};

type Rectangle<_> = {[A in AxisG<_>]: [A] extends [Axis] ? SpanId<A> : undefined};
function setRectangleSide<_, A extends Axis>(rect: Rectangle<_>, a: A, side: SpanId<A>) {
  // Error: "Type 'SpanId<A>' is not assignable to type 'A extends Axis ? SpanId<A> : undefined'."
  // Would be allowed.
  rect[a] = side;
}

Examples

See the use cases for two examples of what would be allowed that currently isn't. I'll wait to see if this suggestion gets any interest before putting forth a proposal beyond "at least A = B should work".

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code (By causing some conditional types to simplify when they didn't before, this change could conceivably introduce errors in code that is doing crazy things with types or remove errors in code that was expecting a particular crazy pattern to produce an error under certain conditions. This is a risk with any change to the type system. I don't think "non-crazy" code is likely to break, but I could be wrong.)
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@jack-williams
Copy link
Collaborator

How would this interact with never short-circuiting? If I understand your proposal correctly then

type Foo<T extends { x: string }> = T extends { x: string } ? string : number

should simplify to string. This would be a change to the current semantics where Foo<never> = never.

@mattmccutchen
Copy link
Contributor Author

I was focusing on non-distributive conditional types. I've edited the initial comment accordingly. I haven't thought carefully about the behavior for distributive conditional types; in fact, I don't understand them very well.

@mattmccutchen
Copy link
Contributor Author

Could one of the TypeScript maintainers please take a look at this and at least give it a "Suggestion" label? There are a few other unlabeled issues; I guess no one must be checking regularly to make sure issues do not pass out of the first page unnoticed.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Jul 31, 2018
@mattmccutchen
Copy link
Contributor Author

Duplicate of #23132.

@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Aug 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants