Search code examples
typescriptnarrowing

Delete optional property after type predicate check while maintaining static analysis


I have an interface I with only optional properties. In my case I is a generic. I need a type narrowing function that still allows to delete the narrowed property. The following code shows the problem.

interface I {
    value?: number
}

function check(i: I): i is I & { value: number } {
    return "value" in i
}

function test(i: I) {
    if (check(i)) {
        i.value += 1 // ok
        delete i.value // error: The operand of a 'delete' operator must be optional.
    }
}

// i as I is not an option to force deletion because it allows the access
// to i.value after deletion
function test2(i: I) {
    if (check(i)) {
        i.value += 1 // ok
        delete (i as I).value // ok
        i.value += 1 // !!! still ok
    }
}

// expected behaviour should be something like
function test(v: I) {
  if ("value" in v) {
    v.value += 5 // ok
    delete v.value // ok
    v.value += 5 // error: understandable and good
  }
}

The problem is, that the narrowing creates a new interface with value as a required type. So I need syntax to say that value can be optional / removed but currently contains a value.

A workaround would be to define a function for deleting that renarrows the value back to optional.

Is there some syntax or pattern to allow the use of the delete keyword?


Solution

  • Unfortunately you can't express what you're doing with a custom type guard function whose return type is of the form i is X for some type X. TypeScript doesn't have a type that means "the value property of type number is optional (so it may be deleted), but known to be present (so it can assigned to number)". The type {value?: number} means the property is optional but possibly missing, and the type {value: number} means the property is known to be present but is required. Neither is appropriate for the return type of a custom type guard function.

    The desired state is possible to achieve via control flow anlysis as you've seen (although I'd change your example to

    function test(i: I) {
        if (typeof i.value !== "undefined") {
            i.value += 5 // ok
            delete i.value // ok
            i.value += 5 // error: understandable and good
        }
    }
    

    instead of if ("value" in i)). But again, there's no appropriate way to represent that post-checked state as giving i a new type.


    I didn't find a relevant TypeScript GitHub issue; so you might want to file a new feature request for a type that works this way. But I doubt it would be implemented, since it's not a very common situation, and the workaround is to use the direct type guard instead of a custom type guard function. If you do decide to file a feature request, you should take care to demonstrate why available workarounds don't suffice.

    Playground link to code