-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Assignability rule for conditional types needs to require check types and distributivity to be identical #27118
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
Comments
This is less about conditional type check types and more about how tuple members/array indexes should be invariant instead of covariant. This is a known tradeoff we make to make passing around arrays of things in a (mostly) readonly fashion easier. |
@weswigham I don't understand: no tuples or arrays are mutated in the example. In fact, since the example works even with distributive conditional types in this case, it can be rewritten without any tuples. I hope you agree that type Wrapped<T> = {readonly value: T};
type MyElement<A> = A extends Wrapped<infer E> ? E : never;
function oops<A, B extends A>(arg: MyElement<A>): MyElement<B> {
return arg; // compiles OK, expected compile error
}
oops<Wrapped<number | string>, Wrapped<string>>(42).slice(); // runtime error
type MyAcceptor<A> = A extends Wrapped<infer E> ? (arg: E) => void : never;
function oops2<A, B extends A>(arg: MyAcceptor<B>): MyAcceptor<A> {
return arg; // compiles OK, expected compile error
}
oops2<Wrapped<number | string>, Wrapped<string>>((arg) => arg.slice())(42); // runtime error Or were you saying that the assignability rule for conditional types contributes in some way to the goal of most generic types being covariant by default? How that would be is not clear to me yet. |
Eyyyea. We're (probably) comparing the check type of a conditional type as unconditionally covariant, when an Or it could be another bug, I'm just speculating since I haven't actually looked into it yet. |
I guess I should have stated in the initial comment, currently the check type of a conditional type is treated as unconditionally bivariant. I'll copy and paste the code from the checker since the file is too big to link to: // Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
if (isTypeIdenticalTo((<ConditionalType>source).extendsType, (<ConditionalType>target).extendsType) &&
(isRelatedTo((<ConditionalType>source).checkType, (<ConditionalType>target).checkType) || isRelatedTo((<ConditionalType>target).checkType, (<ConditionalType>source).checkType))) {
if (result = isRelatedTo(getTrueTypeFromConditionalType(<ConditionalType>source), getTrueTypeFromConditionalType(<ConditionalType>target), reportErrors)) {
result &= isRelatedTo(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(<ConditionalType>target), reportErrors);
}
if (result) {
errorInfo = saveErrorInfo;
return result;
}
} For arrays and objects, covariance is defensible as a useful behavior on the grounds that read-only use of arrays and objects is very common. For conditional types, do we know anything about (1) how often they are genuinely covariant or contravariant and (2) how often banning one or the other form of variance would produce undesired errors compared to desired ones? Measuring the variance would be ideal, but I don't know if it's feasible given all the weird things that TypeScript type inference does. |
EDIT: removing some things that are patently wrong. Currently it seems to be considering the two occurrences of
which feels very wrong. I don't think As a random aside, would these rules work:
taking |
@jack-williams Assuming the conditional types are non-distributive (sigh), you are close. We may want to account for nontransitivity of assignability. For example, consider: function foo<A, B extends A>(arg: A extends number ? {} : never):
B extends number ? {} : never {
return arg;
}
let n: never = foo<any, string>({}); I suspect the concept we would need may be similar to the "definitely assignable" relation that is used to evaluate conditional types. Or we could just say that anyone who writes the above code deserves what they get. |
@mattmccutchen Yes, nontransitivity is definititely going to break things! Additional example: function anotherOne<A, B extends A>(arg: A extends object ? {} : never):
B extends object ? {} : never {
return arg;
}
let n: never = anotherOne<Object, string>({}); |
Have you looked into the distributive case much? I think the covariant case might be ok, but the contravariant case seems unresolvable. type X<A> = A extends object ? true : false;
function distr<A, B extends A>(arg: X<A>): X<B> {
return arg;
}
let n: never = distr<Function, never>(true); |
We'll see what happens - might be too large of a break, but the theoretical justification is strong |
Re #27619 (comment):
I may as well just prepare a pull request and let the TypeScript team run it on the RWC suite. |
types and distributivity to be identical. Fixes microsoft#27118.
PR is #27697. When I ran the test suite, I found the the previous issue in which the bivariance was introduced: #22860. This is basically the example with export class Option<T> {
toVector(): Vector<T> {
return <any>undefined;
}
}
interface Seq<T> {
tail(): Option<Seq<T>>;
}
class Vector<T> implements Seq<T> {
tail(): Option<Vector<T>> {
return <any>undefined;
}
partition2<T, U extends T>(this: Vector<T>, predicate:(v:T)=>v is U): [Vector<U>,Vector<Exclude<T,U>>];
partition2(predicate:(x:T)=>boolean): [Vector<T>,Vector<T>];
partition2<U extends T>(predicate:(v:T)=>boolean): [Vector<U>,Vector<any>] {
return <any>undefined;
}
} |
Is this the same issue or a different one: interface Foo { a: string; }
interface Bar extends Foo { a: "literal"; }
type Cond<T extends Foo> = string extends T['a'] ? true : false;
type True = Cond<Foo>; // okay
type ShouldBeFalse = Cond<Bar> // true?! 😮
type ShouldBeFalseInlined = string extends Bar['a'] ? true : false; // false |
@jcalz Looks unrelated. That's caused by the following rule in // A type S is related to a type T[K], where T and K aren't both type variables, if S is related to C,
// where C is the base constraint of T[K]
if (relation !== identityRelation && !(isGenericObjectType((<IndexedAccessType>target).objectType) && isGenericIndexType((<IndexedAccessType>target).indexType))) {
const constraint = getBaseConstraintOfType(target);
if (constraint && constraint !== target) {
if (result = isRelatedTo(source, constraint, reportErrors)) {
return result;
}
}
} This rule has caused me grief on my project and I am happy to have another reason that might help justify getting rid of it. (Did your example originate in real code?) Feel free to file a separate issue; I didn't see an existing one. |
@jcalz @mattmccutchen Related to that issue: #27470. (I'll leave the discussing of this issue at that as I don't want to go off topic). |
Uh oh!
There was an error while loading. Please reload this page.
TypeScript Version: master (e471856)
Search Terms: assignable assignability conditional type check type checkType distributive distributivity identical unsound
Code
Expected behavior: Compile errors as marked.
Actual behavior: Compiles OK, runtime errors as marked.
Playground Link: link
Related Issues: None found
The text was updated successfully, but these errors were encountered: