microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.59k stars 12.43k forks source link

JSON type #27930

Open streamich opened 5 years ago

streamich commented 5 years ago

Search Terms

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:

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'}
weswigham commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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

streamich commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

... 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 commented 5 years ago

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 commented 5 years ago

@weswigham Exactly!

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

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 commented 5 years ago

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

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

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 commented 5 years ago

Autocompletion for above mentioned example works, too:

image

streamich commented 5 years ago

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

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

NotWearingPants commented 4 years ago

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)