Home > Back-end >  TypeScript type-safety fails with JSON.parse
TypeScript type-safety fails with JSON.parse

Time:10-22

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?

CodePudding user response:

JSON.parse returns an any. And since any contains any type (including undefined), any | undefined is the same as any.

Use as to type the JSON.parse result and you get the expected output:

const person: Person = db.person ? JSON.parse(db.person) as Person : undefined; 
// Error: Type 'Person | undefined' is not assignable to type 'Person'. Type 'undefined' is not assignable to type 'Person'

EDIT: You seem to be unclear about the any type. This essentially switches off all type safety. Every type can be assigned to any and any can be assigned to every type. When you do

const myAnyFunc = (): any => {return undefined;};
const myPerson: Person = myAnyFunc();

this does not create a TypeError. It's nothing special about JSON.parse(), just a 'problem' with anything that returns any. Take a look at this excellent TS book to learn more about any.

CodePudding user response:

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.

  • Related