Repository navigation
Definitely assignable relation should consider some type variable constraints #25883
Description
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)
Activity
jack-williams commentedon Jul 24, 2018
How would this interact with
nevershort-circuiting? If I understand your proposal correctly thenshould simplify to
string. This would be a change to the current semantics whereFoo<never> = never.mattmccutchen commentedon Jul 24, 2018
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 commentedon Jul 26, 2018
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.
mattmccutchen commentedon Aug 3, 2018
Duplicate of #23132.