Skip to content

JSON type #27930

Open
Open
JSON type#27930
@streamich

Description

@streamich

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:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
    This wouldn't change the runtime behavior of existing JavaScript code
    This could be implemented without emitting different JS based on the types of the expressions
    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'}

Activity

changed the title [-]JSON field[/-] [+]JSON type[/+] on Oct 16, 2018
weswigham

weswigham commented on Oct 16, 2018

@weswigham
Member

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).

streamich

streamich commented on Oct 16, 2018

@streamich
Author

@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.

RyanCavanaugh

RyanCavanaugh commented on Oct 16, 2018

@RyanCavanaugh
Member

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

streamich

streamich commented on Oct 16, 2018

@streamich
Author

@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}}
RyanCavanaugh

RyanCavanaugh commented on Oct 16, 2018

@RyanCavanaugh
Member

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?

streamich

streamich commented on Oct 16, 2018

@streamich
Author

@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;
      }
    }
  })[];
}
RyanCavanaugh

RyanCavanaugh commented on Oct 16, 2018

@RyanCavanaugh
Member

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

streamich

streamich commented on Oct 16, 2018

@streamich
Author

... 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.

weswigham

weswigham commented on Oct 16, 2018

@weswigham
Member

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;
streamich

streamich commented on Oct 16, 2018

@streamich
Author

@weswigham Exactly!

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

weswigham commented on Oct 16, 2018

@weswigham
Member

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.

streamich

streamich commented on Oct 16, 2018

@streamich
Author

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

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

streamich commented on Oct 16, 2018

@streamich
Author

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

streamich

streamich commented on Oct 16, 2018

@streamich
Author

Autocompletion for above mentioned example works, too:

image

streamich

streamich commented on Oct 31, 2018

@streamich
Author

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

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

NotWearingPants

NotWearingPants commented on Nov 19, 2019

@NotWearingPants

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @weswigham@RyanCavanaugh@streamich@NotWearingPants

        Issue actions

          JSON type · Issue #27930 · microsoft/TypeScript