Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON type #27930

Open
4 tasks done
streamich opened this issue Oct 16, 2018 · 16 comments
Open
4 tasks done

JSON type #27930

streamich opened this issue Oct 16, 2018 · 16 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@streamich
Copy link

streamich commented Oct 16, 2018

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'}
@streamich streamich changed the title JSON field JSON type Oct 16, 2018
@weswigham
Copy link
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).

@weswigham weswigham added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Oct 16, 2018
@streamich
Copy link
Author

streamich commented Oct 16, 2018

@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
Copy link
Member

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

@streamich
Copy link
Author

streamich commented Oct 16, 2018

@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
Copy link
Member

RyanCavanaugh commented Oct 16, 2018

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
Copy link
Author

streamich commented Oct 16, 2018

@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
Copy link
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
Copy link
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
Copy link
Member

weswigham commented Oct 16, 2018

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
Copy link
Author

streamich commented Oct 16, 2018

@weswigham Exactly!

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

@weswigham
Copy link
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
Copy link
Author

streamich commented Oct 16, 2018

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

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

@streamich
Copy link
Author

streamich commented Oct 16, 2018

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
Copy link
Author

streamich commented Oct 16, 2018

Autocompletion for above mentioned example works, too:

image

@streamich
Copy link
Author

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

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

@NotWearingPants
Copy link

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
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants