Typescript: JSON type

Created on 16 Oct 2018  ·  16Comments  ·  Source: microsoft/TypeScript

Search Terms

  • JSON

Suggestion

Type annotation for JSON in a string.

Use Cases

Let's say you have a string which contains valid JSON object, like so:

const json = '{"hello": "world"}';

How can you type annotate the json variable? Currently, you can mark it as a string:

const json: string = '{"hello": "world"}';

Instead there could be some TypeScript language feature that helps with typing JSON in a string more precisely, for example:

const json: JSON {hello: string} = '{"hello": "world"}';

Examples

Specify that string contains valid JSON.

let json: JSON any;
let json: JSON; // shorthand

Add typings to an HTTP response body.

let responseBody: JSON {ping: 'pong'} = '{"ping": "pong"}';

Add type safety to JSON.parse() method.

let responseBody: JSON {ping: 'pong'} = '{"ping": "pong"}';
let {ping} = JSON.parse(responseBody);
typeof ping // 'pong'

JSON cannot contain complex types.

type Stats = JSON {mtime: Date}; // Error: Date is not a valid JSON type.

Doubly serialized JSON.

let response: JSON {body: string} = '{"body": "{\"userId\": 123}"}';
let fetchUserResponse: JSON {body: JSON {userId: number}} = response;

Get type of serialized JSON string using jsontype keyword.

type Response = JSON {body: string, headers: object};
type ResponseJson = jsontype Response; // {body: string, headers: object}
type ResponseBody = ResponseJson['body']; // string
type ResponseBody = (jsontype Response)['body']; // string

Specify that variable is JSON-serializable.

let serializable: jsontype JSON = {hello: 'world'};
JSON.serialize(serializable); // OK

let nonserializable: object = {hello: 'world'};
JSON.serialize(nonserializable); // Error: 'nonserializable' might not be serializable.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. new expression-level syntax)

Syntax Alternatives

type ResponseRaw = JSON {ping: 'pong'};
type ResponseRaw = json {ping: 'pong'};
type ResponseRaw = string {ping: 'pong'};
type ResponseRaw = json_string {ping: 'pong'};
type ResponseRaw = JSON<{ping: 'pong'}>;
type ResponseRaw = JSON({ping: 'pong'});

type Response = jsontype Response; // {ping: 'pong'}
type Response = typeof Response; // {ping: 'pong'}
type Response = parsed(Response); // {ping: 'pong'}
In Discussion Suggestion

Most helpful comment

BTW, create this tiny NPM package if anyone needs branded JSON strings:

https://github.com/streamich/ts-brand-json

All 16 comments

It seems like you want, generally, refinements on string types. In the vein of #6579 (for specifically regex refinements) or #4895 (for arbitrary nominal refinements).

@weswigham refinements on string type, yes, but this proposal deals specifically with JSON, which is a common use case and—I believe—specific enough that it could actually be implemented.

What are the use cases for writing JSON strings in code instead of writing them as parsed literals?

@RyanCavanaugh I have plenty of mock data for tests as JSON in strings, when you receive response from and API it could be JSON in a string, when you read from a file it could be .json. I'm sure there a re plenty more examples.

Doubly, triply, etc. serialized JSON is another example.

'{"body": "{\"userId\": 123}"}' // JSON {body: JSON {userId: number}}

What I mean is, if you're writing the code, why are you writing them in the error-prone "{ 'x': 'y'}" form instead of the easier { x: 'y' } form?

@RyanCavanaugh I am not, but sometimes you receive your data in that form and you have to deal with it. For example, here is a typical AWS SQS response example:

{
  "Messages": [
    {
      "Body": "{\n  \"Message\" : \"{\\\"assetId\\\":14,\\\"status\\\":\\\"Uploading\\\",\\\"updatedAt\\\":\\\"2018-10-16T08:47:43.538Z\\\"}\",\n }"
    }
  ]
}

(I have removed some fields for brevity. Also, I hope all the escapings are correct. :) )

The above is basically dobly-serialized JSON in Messages[0].Body field. I have no control of this format, but I would like to type annotate it somehow. For example it could be done like so:

interface Response {
  Messages: ({
    Body: JSON {
      Message: JSON {
        assetId: number;
        status: 'Queued' | 'Uploading' | 'Error' | 'Done';
        updatedAt: string;
      }
    }
  })[];
}

sometimes you receive your data in that form

Makes sense - but in that case, we can't really do any valuable typechecking of that JSON at compile-time. Or are you saying you're copying the JSON responses into your test files? Just trying to understand

... we can't really do any valuable typechecking of that JSON at compile-time.

Sure, but code can be annotated at dev time so developer can get all the code completion and error messages that are obvious from static code analysis. For example:

JSON.parse(JSON.parse(JSON.parse(message).Body).Message).assetId; // OK
JSON.parse(JSON.parse(JSON.parse(message).Body).Message).oops; // Error: ...

Or are you saying you're copying the JSON responses into your test files?

Yes.

So you're saying it'd be useful coupled with a JSON.parse overload along the lines of

declare function parse<T>(string: JSON T): T;

@weswigham Exactly!

interface GlobalJSON {
  parse: <T>(str: JSON T) => T;
  stringify: <T>(obj: jsontype T) => T;
}

Along the lines of what people have said in #4895, you can get pretty close with branded strings today:

type JSONString<T> = string & { " __JSONBrand": T };
function parse<T>(str: JSONString<T>): T { return JSON.parse(str as string) as any; };
let responseBody = '{"ping": "pong"}' as JSONString<{ping: 'pong'}>;
parse(responseBody).ping; // OK

there's no automatic creation of them and no automatic validation that your string actually meets the constraint you want the type to imply, but you _can_ flow the type around, at least.

@weswigham How would you annotate JSON.stringify method using branded strings?

function stringify<T>(obj: T): JSON<T> { return JSON.stringify(obj); }

OK, if anyone is interested, here is what I did:

type JSON<T> = string & {__JSON__: T};
declare const JSON: {
  parse: <T>(str: JSON<T>) => T;
  stringify: <T>(obj: T) => JSON<T>;
};

Autocompletion works:

image

Autocompletion for above mentioned example works, too:

image

BTW, create this tiny NPM package if anyone needs branded JSON strings:

https://github.com/streamich/ts-brand-json

It is faster to use JSON.parse of a string literal than to use a JSON object literal:
https://v8.dev/blog/cost-of-javascript-2019#json

So this feature is now a bit more useful (although it is better if the compiler will generate the JSON.parse itself when it sees a JSON literal)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

fwanicka picture fwanicka  ·  3Comments

weswigham picture weswigham  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

zhuravlikjb picture zhuravlikjb  ·  3Comments