Skip to content
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

Type guard for undefined not working for intersection type #28079

Closed
cvuorinen opened this issue Oct 23, 2018 · 5 comments
Closed

Type guard for undefined not working for intersection type #28079

cvuorinen opened this issue Oct 23, 2018 · 5 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@cvuorinen
Copy link

I am using an empty enum as intersection with string to augment a nominal type (according to these instructions https://basarat.gitbooks.io/typescript/docs/tips/nominalTyping.html#using-enums). Otherwise it's working correctly, but I'm getting errors from nullable properties even though there is an if check to make sure it's not undefined or null.

TypeScript Version: 3.2.0-dev.20181023

Search Terms: "intersection nullable guard" "intersection undefined guard" "undefined nominal guard" "guard undefined"

Code

// Regular type alias (working correctly)
type UuidString = string

type DataWithString = {
    id?: UuidString
};

function doSomethingWithString(id: UuidString) {
    // ...
}

function handleDataWithString(data: DataWithString) {
    if (data.id) { // if condition correctly acts as guard against undefined
        doSomethingWithString(data.id) // No error here
    }
}

// nominal type for UUID
// according to https://basarat.gitbooks.io/typescript/docs/tips/nominalTyping.html#using-enums
enum UuidType { }
type Uuid = UuidType & string

type DataWithUuid = {
    id?: Uuid
}

function doSomethingWithUuid(id: Uuid) {
    // ...
}

function handleUuid(data: DataWithUuid) {
    if (data.id) { // if condition not working as guard against undefined
        doSomethingWithUuid(data.id) // <-- Error
    }
}

Expected behavior:
no errors

Actual behavior:

test.ts:33:29 - error TS2345: Argument of type 'undefined' is not assignable to parameter of type 'Uuid'.
  Type 'undefined' is not assignable to type 'UuidType'.

33         doSomethingWithUuid(data.id) // <-- Error
                               ~~~~~~~

Playground Link: (+ select strictNullChecks from options)

Related Issues:

@ghost
Copy link

ghost commented Oct 23, 2018

enum by default is a number type. So UuidType & string is something that is both a number and a string, which can never happen. When you write id?: Uuid, that generates a property whose type is Uuid | undefined. When TypeScript sees a union type it will try to simplify it, such as by removing impossible members; Uuid is impossible so the union simplifies to undefined. The if test won't work because data.id doesn't have any non-undefined types to narrow to.

You may want to send your 👍 to #202.

@ghost ghost added the Working as Intended The behavior described is the intended behavior; this is not a bug label Oct 23, 2018
@jack-williams
Copy link
Collaborator

jack-williams commented Oct 23, 2018

I think another way to interpret the behaviour, according to this comment, is that the an enum type is the union type of its members. As the enum is empty its type is the empty union, which is the same as the never type. So never & string reduces to never. Either way the result is the same as described by Andy.

I would also add an issue on the TypeScript book because this method of writing nominal types seems flawed.

@svieira
Copy link

svieira commented Oct 23, 2018

Can be made to work by switching the enum to a string enum to get the nominality:

enum UuidUniverse { UuidType = '' }  // <-- Now a string enum
type Uuid = UuidUniverse.UuidType & string

type DataWithUuid = {
    id?: Uuid
}

function doSomethingWithUuid(id: Uuid) {
    // ...
}

function handleUuid(data: DataWithUuid) {
    if (data.id) { // if condition now working as guard against undefined
        doSomethingWithUuid(data.id) // <-- Works
    }
}

Symbol-driven branding doesn't work (for the same reasons as the int-based enums don't work) as Andy has already pointed out. Literal-driven branding does work too:

interface Nominal<T/*: must be an enum or a literal */> {
  'nominal structural brand': T
}
enum UuidType {}
type Uuid = string & Nominal<UuidType>;

// ... etc. ...

@ghost
Copy link

ghost commented Oct 23, 2018

enum UuidUniverse { UuidType = '' }  // <-- Now a string enum
type Uuid = UuidUniverse.UuidType & string

If you hover over it you'll see Uuid = UuidUniverse, because:

  • UuidUniverse.UuidType is already a string, so & string has no effect
  • UuidUniverse has only one element and so is equivalent to that element.

@cvuorinen
Copy link
Author

Thanks @svieira, just came here to post that exact same thing as I figured I'll try it based on andys comment on them being different type. But now reading again what @andy-ms says, it means

enum UuidUniverse { UuidType = '' }  // <-- Now a string enum
type Uuid = UuidUniverse.UuidType & string

is equivalent to just

enum Uuid { UuidType = '' }

Not sure if there are any downsides to using just that, it seems to work correctly for my use case. Which is that we get data from another server as JSON and deserialize it, we have typings for the data and there are some id's that are UUIDs and then some other IDs that are string but not UUID. And we had few bugs where we passed the wrong type of IDs to functions that expected an UUID (as we previously just had type Uuid = string). We do not create those IDs in our codebase, they just flow through. So with this kind of typing we can catch those bugs since a value typed as Uuid can be used anywhere a string is expected but plain string cannot be used where Uuid is expected.

Another solution is to use the original code I posted and just add ! in the line that gives the error, e.g. doSomethingWithUuid(data.id!). Not that big a deal I guess. Or anyone have any other suggestions to handling this kind of situation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants