Hi
I've found some weird features in the typed file in TypeScript as to JSON.parse.
// typescript/lib/lib.es5.d.ts
interface JSON {
...
parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
...
The type of the parse function's first parameter, text, is defined as a string
But, in the ECMAScript Language Specification and ECMA 404, null or boolean values could also be considered JSON values. Thus, restricting the type for text as string only should be fixed as like string | null | boolean ?
Or is there any reason for having string type alone?
Thank you
Hi! I'm not part of the TS team; just an interested community member.
There's a template you're supposed to fill out when filing an issue. When you clicked "New Issue" it asked whether you were reporting a bug or requesting a feature, and each choice had a template tailored to it, with specific instructions about what constitutes a proper issue report. These instructions help the team to triage and respond properly to the large volume of issues filed. Without filling out the template completely, you are likely to get your issue closed as unactionable.
About this issue: it seems that you're confusing JSON text, which is always a string, with JSON values, which are the values represented by the text. JSON.parse(text) converts JSON text to a JSON value, while JSON.stringify(value) converts a JSON value to JSON text. null is a valid JSON value, whose corresponding JSON text is "null", a string. So the first argument to JSON.parse() is intended to be a string only.
It is true that, at runtime, you can call JSON.parse(false) and it will produce false as an output. That's because JSON.parse() coerces the argument to a string before parsing it. But TypeScript usually takes the position that just because something doesn't explode at runtime, it doesn't make it good code. The code Math.cos(true) has a deterministic output of 0.5403023058681398, but Math.cos() really should take a number, not boolean, and someone writing Math.cos(true) is probably doing something wrong. In other words, valid-according-to-the-spec and correctly-typed are two different things. So JSON.parse(false) and Math.cos(true) result in TS warnings: false is not a string, and true is not a number. If you want to write such things and have TS not complain, it's best to be explicit about the coercion: JSON.parse(String(false)) and Math.cos(Number(true)).
Hope that helps; good luck!
@jcalz beating me to linking my own SO post 馃槄
About this issue: it seems that you're confusing JSON text, which is always a string, with JSON values, which are the values represented by the text. JSON.parse(text) converts JSON text to a JSON value, while JSON.stringify(value) converts a JSON value to JSON text. null is a valid JSON value, whose corresponding JSON text is "null", a string. So the first argument to JSON.parse() is intended to be a string only.
This is actually a really clear and nice explanation, even better than the specification.
But then what about this case?
JSON.parse(''); // Uncaught SyntaxError
JSON.parse(String('')); // Uncaught SyntaxError
Both give you an error, so I've convinced myself the first example's wrong by your explanation. JSON.parse converts the JSON text to the JSON value. But why was the second one also wrong as well?
It's actually quite confusing for me to grasp the concept of what could be JSON texts, from reading ECMA-404
@jcalz Thanks for your comments.
It helps me in understanding the typescript.
Actually, the case of @moonformeli had happened to me at runtime.
Typescript complained about the nullable variable for parse.
So I had changed my code null to empty string ''.
That had made me confused about understanding the Typescript and the type of JSON.parse.
In this situation, there is no other way than the developer should use the API carefully.
It's actually quite confusing for me to grasp the concept of what could be JSON texts, from reading ECMA-404
Maybe it's easier to think in the other direction? To a first approximation: a string s is valid JSON text if and only if there's some valid JSON value v where JSON.stringify(v) is equal to s. A valid JSON value is either null, a string, a number, a boolean, an array of JSON values, or a plain object whose properties are JSON values.
Is "" valid JSON text? No. There is no JSON value v where JSON.stringify(v) === "" is true. You might think that the empty string "" is such a value v, but JSON.stringify("") is not "", but "\"\"", which is not the empty string, but a string containing a pair of quotation marks.
The point is that JSON.parse() is meant to act on a string which was serialized into JSON text by some other source. It's not meant for acting on arbitrary string values. If you find yourself trying to parse arbitrary strings as JSON, you are either doing something wrong or you need to be careful to check for validity.
there is no other way than the developer should use the API carefully
Yes, that's true.
There's no way in TypeScript to represent the subtype of string corresponding to properly-formatted JSON text, so we can't restrict the signature of JSON.parse() to only accept such strings.
There's also no way for TypeScript to anticipate all runtime errors, nor is there currently a checked exception feature to say something like parse(text: string): any throws SyntaxError. The way the language is currently implemented, the return type of a function that throws an error is never, and never is absorbed into any union (e.g., a function that sometimes returns a number and sometimes throws an error returns a value of number | never which is immediately collapsed to number) so there's not much use in even writing the signature as parse(text: string): SomeJSONValueType | never.
Throwing runtime exceptions is not everyone's idea of a proper way to handle failure. Array.prototype.find() does not throw a RangeError if the argument cannot be found; it returns undefined. If you really want a more error-handling-explicit version of JSON.parse() you can write one yourself:
interface JSONParseSuccess {
success: true,
value: any
};
interface JSONParseFailure {
success: false,
value?: undefined
}
type JSONParseResult = JSONParseSuccess | JSONParseFailure;
function JSONParse(...args: Parameters<typeof JSON.parse>): JSONParseResult {
try {
return { success: true, value: JSON.parse(...args) };
} catch (e) {
if (e instanceof SyntaxError) return { success: false };
throw e;
}
}
This will force you to handle success and failure. It is considerably more annoying to use than plain JSON.parse(), but you will not get unexpected SyntaxErrors at runtime:
function check(str: string) {
const result = JSONParse(str);
if (result.success) {
console.log("Successfully parsed (" + str + "), value is:");
console.log(result.value);
} else {
console.log("Failed to parse (" + str + "), sorry");
}
}
check(""); // Failed to parse (), sorry
check("\"\""); // Successfully parsed (""), value is: <empty string>
check("null"); // Successfully parsed (null), value is: null
check("undefined"); // Failed to parse (undefined), sorry
check("{a: 1}"); // Failed to parse ({a: 1}), sorry
check("{\"b\":2}"); // Successfully parsed ({"b":2}), value is: Object { b: 2 }
Cheers!
Most helpful comment
Hi! I'm not part of the TS team; just an interested community member.
There's a template you're supposed to fill out when filing an issue. When you clicked "New Issue" it asked whether you were reporting a bug or requesting a feature, and each choice had a template tailored to it, with specific instructions about what constitutes a proper issue report. These instructions help the team to triage and respond properly to the large volume of issues filed. Without filling out the template completely, you are likely to get your issue closed as unactionable.
About this issue: it seems that you're confusing JSON text, which is always a
string, with JSON values, which are the values represented by the text.JSON.parse(text)converts JSON text to a JSON value, whileJSON.stringify(value)converts a JSON value to JSON text.nullis a valid JSON value, whose corresponding JSON text is"null", astring. So the first argument toJSON.parse()is intended to be astringonly.It is true that, at runtime, you can call
JSON.parse(false)and it will producefalseas an output. That's becauseJSON.parse()coerces the argument to astringbefore parsing it. But TypeScript usually takes the position that just because something doesn't explode at runtime, it doesn't make it good code. The codeMath.cos(true)has a deterministic output of0.5403023058681398, butMath.cos()really should take anumber, notboolean, and someone writingMath.cos(true)is probably doing something wrong. In other words, valid-according-to-the-spec and correctly-typed are two different things. SoJSON.parse(false)andMath.cos(true)result in TS warnings:falseis not astring, andtrueis not anumber. If you want to write such things and have TS not complain, it's best to be explicit about the coercion:JSON.parse(String(false))andMath.cos(Number(true)).Hope that helps; good luck!