Reputation: 21259
JSON.parse(aString)
is typed as any
by default. However, any
also includes functions, dates, regexes and other classes which cannot be parsed.
Now I suspect we can be more accurate if we make use of the conditional types introduced in ts 2.8 and inference of the 3.0 but I am a little bit stuck on how to do it. Perhaps there is some existing basic type I am missing, could anyone advise me?
const good: BasicType = JSON.parse(aString); // what I need
const bad: any = JSON.parse(aString); // what I do have now
[EDIT] It is now possible to have recursive types and therefore to type JSON objects properly, typefest being one example of implementation.
Upvotes: 4
Views: 1746
Reputation: 9323
I found that it's usually safer than a type guard to use a function that extract the data structure defined by the desired type. I call this function a type extractor:
interface ExpectedType {
key: string;
}
// Returns a variable of the type if the arg is ExpectedType, otherwise null.
function extractExpectedType(arg: unknown): ExpectedType | null {
if (
typeof arg === "object" &&
arg !== null &&
"key" in arg &&
typeof arg.key === "string"
) {
return { key: arg.key };
}
return null;
}
const obj = extractExpectedType(JSON.parse('{"key": "b"}'));
if (obj === null) {
throw new Error("Unexpected Type");
}
console.log(obj);
//^? const obj: ExpectedType
Note that, a more straight thinking is to use type narrowing right after retrieving the result of parsing. However, this often doesn't work due to a limitation in TypeScript:
interface ExpectedType {
key: string;
}
const obj: unknown = JSON.parse('{"key": "value"}');
if (typeof obj === "object" && obj !== null && "key" in obj && typeof obj.key === "string") {
obj;
//^? const obj: object & Record<"key", unknown>
}
Upvotes: 2
Reputation: 21259
2024, here is how I solved my problem using an assert with a type guard:
const person = JSON.parse('{"name":"john","surname":"smith"}');
assert(isPerson(person), "Unable to extract a person from the json");
// now person is typed
Full code
/**
* Example of use:
* assert(isNumber(value), 'Value must be a number');
* @param condition
* @param message
*/
export function assert(
condition: boolean,
message: string = 'Assertion failed'
): asserts condition {
if (!condition) {
throw new Error(message);
}
}
// My type and a type guard
interface Person {
name: string;
surname: string;
}
function isPerson(value: unknown): value is Person {
return value instanceof Object && "name" in value && "surname" in value;
}
// Then
const person = JSON.parse('{"name":"john","surname":"smith"}');
assert(isPerson(person), "Unable to extract a person from the json");
console.log(person.name, person.surname); // properly typed
Upvotes: 0
Reputation: 327624
As far as I know there is no built-in type name in the TypeScript standard library that corresponds to types that can be serialized-to/deserialized-from JSON. GitHub issue microsoft/TypeScript#1897 suggests adding one, although it doesn't look like there has been much movement there.
Luckily it's easy enough for your to create your own type for this, such as this one suggested in a comment in the aforementioned GitHub issue:
type AnyJson = boolean | number | string | null | JsonArray | JsonMap;
interface JsonMap { [key: string]: AnyJson; }
interface JsonArray extends Array<AnyJson> {}
And then you can use declaration merging to overload the parse()
method to return AnyJson
instead of any
:
interface JSON {
parse(
text: string,
reviver?: ((this: any, key: string, value: any) => any) | undefined
): AnyJson;
}
(note that if your code is in a module you will need to use global augmentation to get the above to work)
And let's see if it works:
const x = JSON.parse('{"id": 123}'); // x is AnyJson
Since x
is known to be AnyJson
and not any
, you could now do type guards on it and the compiler should be more aware of its possible structures:
if (x !== null && typeof x === "object" && !Array.isArray(x)) {
// x narrowed to JsonMap
Object.keys(x).forEach(k => {
const y = x[k];
// y is also AnyJson
console.log("key=" + k + ", value=" + String(y));
});
// key=id, value=123
}
So that all works. That being said, the type AnyJson
isn't a whole lot more specific than any
, practically speaking. I suppose you gain the benefit of excluding functions/methods from possible values/elements/properties, which isn't nothing... it just might not be useful enough to warrant modifying the TypeScript standard library. As said in this comment, the AnyJson
type is "within spitting distance of any
".
Upvotes: 4