Search code examples
javascriptjsontypescripttype-safety

TypeScript type-safety fails with JSON.parse


Am I wrong or does type-safety get thrown out in TypeScript when parsing JSON?

I should be getting an error here, but I don't:

interface Person {
  name: string
}

const person: Person = somePossibleFalsey ? JSON.parse(db.person) : undefined

The above does not fail a type check, when I believe it should. The db.person variable may not be present, which could render person as undefined. But Person should not be undefined. As far as I can tell this is because I'm using JSON.parse.

Just to confirm that I should get an error, here is another snippet which correctly gives me an error:

const person: Person = Math.random() > .5 ? { name: 'Arthur' } : undefined

The above code produces the appropriate TypsScript error:

Type '{ name: string; } | undefined' is not assignable to type 'Person'.
  Type 'undefined' is not assignable to type 'Person'.ts(2322)

Why is JSON.parse allowing type-safety to fail? Or is there something else at play here?


Solution

  • I think the core of the question is why the second term of the ternary expression, undefined, is allowed to be one of the alternatives that is assigned to Person. This Github issue describes this as working as intended. The entire ternary expression gets the union of the first return type and the second, i.e. any | undefined, which collapses to any, which in turn makes it a valid assignment. (any being the weakest type of all). Conversely, if this were to be wrapped in an if-else block, the error would go off:

    let n: number;
    
    let x: any = {}
    n = x.z ? x.z : undefined // no error
    
    let y: any = {}
    if (y.z) {
        n = y.z
    } else {
        n = undefined // error
    }
    

    (source)

    I’m thinking the inconsistency here is that, in the first case, the entire ternary expression is inferred as any | undefined which collapses to just any; the compiler therefore sees a single return any and all is right with the world (even though it’s not). In the second case there are two return sites: one is return any which again is fine, and one is return undefined which is not.

    I think the challenge is that a ternary isn't really a statement but an expression. An expression must have a single type; in this case that type happens to be any | undefined which is unsound simply by the nature of any. Changing this, I suspect, would be very difficult: what happens when you have a ternary in the middle of a statement, e.g. as an argument to a function? What happens when there are several ternaries in the same statement [...]

    In order to type check the above the way you suggest, the compiler would basically have to duplicate the statement and individually type check it for every combination of true/false for every single ternary in the statement. You'd have a combinatorial explosion.

    The current behavior is, I suspect, the best we're going to get.