TypeScript Version:
$ tsc --version
Version 3.3.0-dev.20181122
Search Terms:
unexpected, exclude
Code
/*
Some basic definitions of unions
*/
type Union = 'a' | 'b';
type Product<A extends Union, B> = { f1: A, f2: B};
type ProductUnion = Product<'a', 0> | Product<'b', 1>;
/*
These work as I would expect. Each element in the union is mapped
the complement or the double complement with nested Exclude
*/
type UnionComplement = {
[K in Union]: Exclude<Union, K>
};
// {a: "b"; b: "a"}
type UnionComplementComplement = {
[K in Union]: Exclude<Union, Exclude<Union, K>>
};
// {a: "a"; b: "b"}
/*
This also works as I would expect
*/
type ProductComplement = {
[K in Union]: Exclude<ProductUnion, { f1: K }>
};
// {a: Product<'b', 1>; b: Product<'a', 0>}
/*
Double complement on the other hand doesn't work
*/
type ProductComplementComplement = {
[K in Union]: Exclude<ProductUnion, Exclude<ProductUnion, { f1: K }>>
};
// {a: ProductUnion; b: ProductUnion}
/*
Explicit inlining works as I would expect
*/
type First = Exclude<ProductUnion, Exclude<ProductUnion, { f1: 'a' }>>;
// {f1: 'a'; f2: 0}
type Second = Exclude<ProductUnion, Exclude<ProductUnion, { f1: 'b' }>>;
// {f1: 'b'; f2: 1}
/*
Making parametrized types works as I would expect
*/
type Complementor<T> = {
[K in Union]: Exclude<T, { f1: K }>
};
type DoubleComplementor<T> = {
[K in Union]: Exclude<T, Exclude<T, { f1: K }>>
};
type Complement = Complementor<ProductUnion>;
// {a: Product<'b', 1>; b: Product<'a', 0>}
type DoubleComplement = DoubleComplementor<ProductUnion>;
// {a: Product<'a', 0>; b: Product<'b', 0>}
Expected behavior:
I would expect the parametrized and non-parametrized type to work the same way but that's not
the case.
Actual behavior:
See the comments.
Playground Link: link
Related Issues: Some issues related to Pick and Exclude but didn't seem to apply.
I've run into a similar (likely related) issue, probably also having to do with distribution over Exclude:
const f = <U extends Exclude<any, Function>>(a: U | ( () => U )): U => {
if (typeof a === "function" ) {
return a(); // Cannot invoke an expression whose type lacks a call signature.
// Type '(() => U) | (U & Function)' has no compatible call signatures.
}
return a;
};
My understanding is that U & Function would expand into Exclude<any, Function> & Function, which would presumably be never.
@riggs I'm confused by what Exclude<any, Function> can mean? I think your intent is to exclude function types. Wouldn't a conditional type be better in that case?
type NonFunction<U> = U extends Function ? never : U;
Ya looking at this I'm not really sure how I would make sense of it so I'm not sure what hope the compiler has.
From lib.es5.d.ts:
type Exclude<T, U> = T extends U ? never : T;
I think this is a smaller repro:
type Union = 'a' | 'b';
type Z<K extends Union> = false extends ([Union] extends [K] ? never : false) ? 'LEFT' : 'RIGHT';
If you hover over Z it appears to have already resolved to RIGHT.
The issue is that the conditional type is getting marked as non-deferred and it seems to be eagerly resolving using the constraint of K. Related code here:
// If this is a distributive conditional type and the check type is generic we need to defer
// resolution of the conditional type such that a later instantiation will properly distribute
// over union types.
const isDeferred = root.isDistributive && maybeTypeOfKind(checkType, TypeFlags.Instantiable);
The actual branch that is getting selected is later:
// Return trueType for a definitely true extends check. The definitely assignable relation excludes
// type variable constraints from consideration. 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.
if (checkTypeRelatedTo(checkType, inferredExtendsType, definitelyAssignableRelation, /*errorNode*/ undefined)) {
return instantiateType(root.trueType, combinedMapper || mapper);
}
A candidate fix is:
const isDeferred = maybeTypeOfKind(extendsType, TypeFlags.Instantiable)
|| root.isDistributive && maybeTypeOfKind(checkType, TypeFlags.Instantiable);
but I think this is probably incomplete and there are other cases to deal with.
@weswigham I think you have an open PR that would fix this, right? (#27932)
Pardon the side-track: Why wrap Union & K in a 1-tuple?
Essentially the compiler is trying to eagerly resolve the conditional type using wildcard types, substituting type parameters for any. For example: K ==> any, or [K] ==> [any]. The compiler knows that any conditional type with a wildcard extends type resolves to the wildcard type, but it misses the case when the wild card is wrapped in a 1-tuple.
(I think).
Amazing turnaround. Thanks @ahejlsberg and team.
Most helpful comment
Amazing turnaround. Thanks @ahejlsberg and team.