Skip to content

2.9 and 3.0 reduce type of optional branded string member as undefined #25709

Closed
@dcolthorp

Description

@dcolthorp

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

Activity

ahejlsberg

ahejlsberg commented on Jul 17, 2018

@ahejlsberg
Member

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" and string & number (a value can't be both "a" and "b" at the same time, nor can it be both a string and a number). An empty intersection type is effectively the same as the never type.

Since an enum is a subtype of number, the string & Brand type in your example is a type that has no possible values and it therefore becomes never.

Generally the recommended way to create branded types is to intersect with an object type:

type BrandedString = string & { __brand__: void };
dcolthorp

dcolthorp commented on Jul 17, 2018

@dcolthorp
Author

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

typescript-bot commented on Jul 27, 2018

@typescript-bot
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @dcolthorp@ahejlsberg@typescript-bot

        Issue actions

          2.9 and 3.0 reduce type of optional branded string member as undefined · Issue #25709 · microsoft/TypeScript