Closed
Description
TypeScript Version: 3.0.0-dev.20180712, 3.0-rc, 2.9. Our original code worked fine in 2.8 with strictNullChecks
enabled.
Search Terms: strictNullChecks, branding, enum, optional, intersection, undefined
Code
// strictNullChecks must be enabled to see this problem
enum Brand {}
type BrandedString = string & Brand;
export const fromString = (s:string) => s as BrandedString
interface ComplexType {
optional?: BrandedString;
}
const example: ComplexType = {
optional: fromString("foo")
};
Expected behavior: Example type checks
Actual behavior: Example does not type check TypeScript thinks optional
is has type undefined
.
Playground Link: Ensure strictNullChecks is enabled. Link here
Related Issues: #25179 looks similarish
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
ahejlsberg commentedon Jul 17, 2018
This is working as intended and is the same issue as #24846. The compiler now removes empty intersection when they occur in union types. An empty intersection type is a type that is known to have zero possible values. Examples include
"a" & "b"
andstring & number
(a value can't be both"a"
and"b"
at the same time, nor can it be both astring
and anumber
). An empty intersection type is effectively the same as thenever
type.Since an enum is a subtype of
number
, thestring & Brand
type in your example is a type that has no possible values and it therefore becomesnever
.Generally the recommended way to create branded types is to intersect with an object type:
dcolthorp commentedon Jul 17, 2018
Aha! Well, reducing empty intersection types to never certainly makes a lot of sense. I love it – intersecting enums always seemed like hack. However, the way in which that manifests today is pretty confusing, because you only get feedback about the issue in certain circumstances, sometimes far removed from the definition of the original type. (In our case it was a
Pick
of a compound type with a branded field, 2 file hops away.)It seems that if an empty intersection is effectively the same as never, it should effectively be the same as never everywhere. There are probably reasons I'm not appreciating for not reducing invalid intersections to never right away, but the property of having an intersection behave uniformly does seem really appealing.
If TypeScript had reported my intersection type to be
never
when I hovered over it while debugging the issue I would have had a very strong hint about the root cause, and switched to an object intersection brand. Actually, the brand never would have survived as an enum because we would have caught the problem right away.With the current behavior, user-defined intersection types seem to work just fine until you happen to want to e.g. use them for an optional property. Hovering shows the type definition, non-unioned uses work as they used to, and only in some circumstances do they behave in a matter inconsistent with every other type I've written.
But assuming there's more to the story than meets the eye, it would have been nice to have had some sort of hint that there was an issue with my branded type. Either a warning or even just a note in the tooltip for the type would have helped clarify the situation for me.
Thanks for the quick reply and the excellent work!
typescript-bot commentedon Jul 27, 2018
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.