Reputation: 15562
This is hard to explain, but I'm trying to filter out a certain type in a subfield of an object type.
Exclude<T, U>
doesn't seem to work with this kind of nesting:
type PrivateName = { kind: "PrivateName" };
type Identifier = { kind: "Identifier" };
type NamedNode = {
name: PrivateName | Identifier;
};
type PrivateNamedNode = {
name: PrivateName;
};
declare const privateName: PrivateName;
type NamedNodeNonPrivate = Exclude<NamedNode, PrivateNamedNode>;
// I expected this next line to error, but it doesn't!
const x: NamedNodeNonPrivate = { name: privateName };
Why didn't the last line error? Should I open a bug report?
Tested on TS versions: 3.5.1, 3.3.3, 3.1.6, and 3.0.1. playground link.
The definition of Exclude<T, U>
is
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
It seems to be inert in my example because NamedNode
doesn't extend { name: PrivateName }
.
Changing the definition as follows produces the desired error message. Unfortunately, this isn't a refactor I can do manually to the whole codebase:
type NamedNode = {
name: PrivateName
} | {
name: Identifier
}
The Curry-Howard Isomorphism tells us that product types are like conjunction and sum types are like disjunction.
The following holds in Boolean logic:
A & (B or C) implies (A & B) or (A & C)
So it seems reasonable that the two definitions of NamedNode would be equivalent. Is this lack of distributivity:
Upvotes: 0
Views: 393
Reputation: 250136
{ name: PrivateName | Identifier; };
and { name: PrivateName } | { name: Identifier }
are equivalent only in the trivial case when there object type contains only this property. The common use case is that the different branches of the union will hold different type.
Regardless of this, conditional types do distribute but only over naked type parameters. No magic distribution occurs over properties of every type. And since the relation NamedNode extends PrivateNamedNode
is not true no types will be excluded (no type can be excluded since NamedNode
is not a union, even if the relation were true, you would get never
since then the whole type would be excluded and you would be left with nothing).
We can build a type that excludes types from properties of an object:
type PrivateName = { kind: "PrivateName" };
type Identifier = { kind: "Identifier" };
type NamedNode = {
name: PrivateName | Identifier;
};
type PrivateNamedNode = {
name: PrivateName;
};
declare const privateName: PrivateName;
type ExcludePropertyTypes<T, TExclude extends Partial<T>> {
[P in keyof T]: Exclude<T[P], TExclude[P]>
}
type NamedNodeNonPrivate = ExcludePropertyTypes<NamedNode, PrivateNamedNode>;
// Errors now (as intended)
const x: NamedNodeNonPrivate = { name: privateName };
Upvotes: 1