Determining the underlying type of a generic Type with TypeScript - json

Consider the following interface within TypeScript
interface IApiCall<TResponse> {
method: string;
url: string;
}
Which is then used within the following method;
const call = <TResponse>(api: IApiCall<TResponse>): void => {
// call to API via ajax call
// on response, grab data
// use JSON.parse(data) to convert to json object
return json as TResponse;
};
Now we use this for Type safety within our methods so we know what objects are being returned from the API. However, when we are returning a single string from the API, JSON.parse is converting the string '12345' into a number, which then breaks further down the line when we are trying to treat this as a string and use value.trim() yet it has been translated into a number.
So ideas to solve this so that we are not converting a string into a number.
How can we stop JSON.parse from converting a single string value into a number?
If using JSON.parse, we check the type of TResponse and compare it against the typeof of json generated.
if (typeof (json) !== typeof(TResponse))...
However there doesn't seem to be an obvious way to determine the generic type.

Question 1: How can we stop JSON.parse() from converting a single string value into a number?
JSON is a text format, so in JSON.parse(x), x needs to be a string. But JSON text represents data of not-necessarily-string types. It sounds like you might be making a category mistake, by confusing a thing with its representation.
If you convert the number 12345 to JSON (JSON.stringify(12345)) you will get the string "12345". If you parse that string, (JSON.parse("12345")), you will get the number 12345 back. If you wanted to get the string "12345", you need to encode it as JSON ( JSON.stringify("12345")) as the string "\"12345\"". If you parse that ( JSON.parse('"12345"') you will get the string "12345" out.
So the straightforward answer to the question "How can we stop JSON.parse() from converting a single string value into a number" is "by properly quoting it". But maybe the real problem is that you are using JSON.parse() on something that isn't really JSON at all. If you are given the string "12345" and want to treat it as the string "12345", then you don't want to do anything at all to it... just use it as-is without calling JSON.parse().
Hope that helps. If for some reason either of those don't work for you, you should post more details about your use case as a Minimal, Complete, and Verifiable example.
Question 2: How do we determine that the returned JSON-parsed object matches the generic type?
In TypeScript, the type system exists only at design time and is erased in the emitted JavaScript code that runs later. So you can't access interfaces and type parameters like TResponse at runtime. The general solution to this is to start with the runtime solution (how would you do this in pure JavaScript) and help the compiler infer proper types at design time.
Furthermore, the interface type IApiCall
interface IApiCall<TResponse> {
method: string;
url: string;
}
has no structural dependence on TResponse, which is not recommended. So even if we write good runtime code and try to infer types from it, the compiler will never be able to figure out what TResponse is.
In this case I'd recommend that you make the IApiCall interface include a member which is a type guard function, and then you will have to write your own runtime test for each type you care about. Like this:
interface IApiCall<TResponse> {
method: string;
url: string;
validate: (x: any) => x is TResponse;
}
And here's an example of how to create such a thing for a particular TResponse type:
interface Person {
name: string,
age: number;
}
const personApiCall: IApiCall<Person> = {
method: "GET",
url: "https://example.com/personGrabber",
validate(x): x is Person {
return (typeof x === "object") &&
("name" in x) && (typeof x.name === "string") &&
("age" in x) && (typeof x.age === "number");
}
}
You can see that personApiCall.validate(x) should be a good runtime check for whether or not x matches the Person interface. And then, your call() function can be implemented something like this:
const call = <TResponse>(api: IApiCall<TResponse>): Promise<TResponse | undefined> => {
return fetch(api.url, { method: api.method }).
then(r => r.json()).
then(data => api.validate(data) ? data : undefined);
};
Note that call returns a Promise<Person | undefined> (api calls are probably asynchronous, right? and the undefined is to return something if the validation fails... you can throw an exception instead if you want). Now you can call(personApiCall) and the compiler automatically will understand that the asynchronous result is a Person | undefined:
async function doPersonStuff() {
const person = await call(personApiCall); // no <Person> needed here
if (person) {
// person is known to be of type Person here
console.log(person.name);
console.log(person.age);
} else {
// person is known to be of type undefined here
console.log("File a missing Person report!")
}
}
Okay, I hope those answers give you some direction. Good luck!

Type annotations only exist in TS (TResponse will be nowhere within the output JS), you cannot use them as values. You have to use the type of the actual value, here it should be enough to single out the string, e.g.
if (typeof json == 'string')

Related

Is there a way to avoid returning `any` within a JSON.parse reviver?

I have a project using TypeScript and ESLint. I need to deserialize a JSON string, and want to take advantage of the optional reviver parameter. A reviver function basically lets you conditionally transform values as part of the JSON deserialization.
The signature of the reviver function is defined as part of the JSON.parse specification, which is:
JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any
In particular: it takes in a value of type any and returns a value of type any.
const deserializedValue: unknown = JSON.parse(serializedValue, (key, value) => {
if (value === 'foo') {
return 'bar
}
return value
}
I'm scolded by ESLint because when I write return value I am returning something of any type:
5:4 error Unsafe return of an `any` typed value #typescript-eslint/no-unsafe-return
Is there a way for me to programmatically avoid the linting complaint about any types within the constraints of the unknown nature of deserialization, or do I have to disable the linting rule for that line?
eslint is a bit overzealous here. Or at least that rule doesn't apply well to this case.
Parsing JSON is an inherently type unsafe process. In this case the any is just passed through from the argument type, and the function is typed in a place you can't control to return any.
So I'd probably just cast it to unknown like:
return value as unknown
Which sort of makes it clear that "I don't know or care what this is". And the return type does matter because anything matches any and the return type is used in the return type of JSON.parse().
This seems to work.
But that's probably not that much better than disabling the rule for that line either. Which is right is more a matter of opinion.
But still, I'd go with the as unknown cast.

JSON serialization with strong type checks enabled in the Dart Linter

I decided to turn on stricter type checks and turn on a lot more Linter rules in Dart. Currently, I am wondering what the best practice is regarding serializing JSON and strong type checks.
I receive a JSON response and covert it to the the typical Map<String, dynamic> form (called response in the examples below). When transforming the JSON response to a pre defined model class I use(d) this code (small sample):
id: response['entity_id'],
name: response['attributes']['friendly_name'],
state: response['state'],
supportedStates: supportedStatesConverter(
response['attributes']?['supported_features'] ?? 0,
),
timestamp: DateTime.tryParse(response['last_updated'])?.toUtc(),
requiresPin:
response['attributes']?['code_format'] != null,
The linter does not like this, because:
It wants me to avoid making method calls on dynamic properties
It does not allow me to assign dynamic arguments to typed arguments
I solved it by casting every received dynamic response to a type with the as keyword. The code above then looks like this:
id: tryCast<String>(response['entity_id']),
name: tryCast<String>(
tryCast<Map<String, dynamic>>(
response['entity_id'],
)?['friendly_name'],
),
state:
tryCast<String>(
response['state'],
),
supportedStates: supportedStatesConverter(
tryCast<int>(response['attributes']['supported_features']) ?? 0,
),
timestamp:
DateTime.tryParse(tryCast<String>(response['last_updated']) ?? '')
?.toUtc(),
requiresPin: tryCast<Map>(response['attributes'])?['code_format'] != null,
The tryCast function is defined as follows:
Type? tryCast<Type>(dynamic value) {
try {
return value as Type;
} on TypeError catch (_) {
logger.e('Tried converting $value to $Type');
return null;
}
}
(It takes a dynamic argument, tries to cast it to a typed argument and if it fails it returns null.)
The way I currently do it feels quite verbose and I am wondering if there are best practices. Searching online did not make me wiser. How should I go about this?

What is "well-typed" in TypeScript?

In the below site, TypeScript 2.9 supports well-defined JSON.
https://blogs.msdn.microsoft.com/typescript/2018/05/31/announcing-typescript-2-9/#json-imports
What's the meaning of a "well-typed" JSON? From my knowledge, JSON has 6 valid data types: string, number, object, array, boolean, and null. So, I think every JSON data type is well-typed or well matched to TypeScript basic types. That's why I cannot figure out the exact meaning of "well-typed".
On the contrary to this, what is "not-well-typed" json?
Well, the example explains it:
// ./src/settings.json
{
"dry": false,
"debug": false
}
// ./src/foo.ts
import settings from "./settings.json";
settings.debug === true; // Okay
settings.dry === 2; // Error! Can't compare a `boolean` and `number`
Since any JSON property can be of any JSON type, you might think that comparing settings.dry === 2 wouldn't cause any compilation error. It will just fail at runtime.
But it actually will issue a compile-time error, because TypeScript will infer the types from the JSON, and will thus prevent you from doing silly things such as comparing a boolean variable to a number: that doesn't make sense.
Just as if you had in your TypeScript:
const settings = {
dry: false,
debug: false
}
if (settings.dry === 2) { ... }

How can I get JSON.parse result covered by the Flow type checker?

I'm new to working with flow. I'm trying to get as close to 100% flow coverage as possible on a project, and one thing I can't figure out is how to handle is JSON.parse.
type ExampleType = {
thingOne: boolean,
thingTwo: boolean,
};
const exampleVariable: ExampleType = JSON.parse(
'{thingOne: true, thingTwo: false}'
);
So I have a type, and I receive a string from another source, and I parse it and expect it to be of that type.
The whole JSON.parse(...) section is marked as "Not covered by flow".
Is there a way to get a file to 100% flow coverage if JSON.parse is used in that file? How? What exactly is flow saying when it says that line isn't covered?
The problem is that JSON.parse returns any. Here is the signature:
static parse(text: string, reviver?: (key: any, value: any) => any): any;
Flow cannot guarantee that the assignment of the parse result to the type ExampleType is correct because who knows what will come out when you parse incoming JSON?
But you can get coverage to 100% if you parse with flow-validator instead. When parsing a string as far as Flow knows that string could have come from anywhere. So there is no static guarantee that the JSON data in the string has the shape that you expect. What flow-validator does it to provide an API to describe a validation schema for your data instead of a type. The schema is checked at runtime while parsing. Flow-validator automatically generates a static type from your schema, and assigns the result from a successful parse to that type. Here is what your example looks like with flow-validator:
import { boolean, object } from "flow-validator"
const ExampleSchema = object({
thingOne: boolean,
thingTwo: boolean
})
const exampleVariable = ExampleSchema.parse(
'{"thingOne": true, "thingTwo": false}'
)
You can check and see that Flow infers the correct type for exampleVariable, and your Flow coverage is now at 100%. If the JSON data does not have the correct shape then ExampleSchema.parse will throw an error.
You can get a type from the schema like this:
type ExampleType = typeof ExampleSchema.type
This version of ExampleType is just like the one in your original example. Extracting a type automatically saves you from having to write the shape for your data structure twice, and it also guarantees that the static type stays in sync with the runtime validation schema.

Replacing data fields with code in JSON.stringify?

So you can replace a property with a number, string, array, or object in JSON.stringify, like so:
var myObj = {
'allstar': aFunction;
}
function myReplacer(key, value) {
if(key === 'allstar') {
return 'newFunction()';
}
}
JSON.stringify(myObj, myReplacer); //returns '{"allstar": "newFunction()"}'
But can you change it so that it instead returns '{"allstar": newFunction()}' (without the quotes around newFunction)?
I assume typeof aFunction == "function"? If so, even JSON.stringify(myObj) will not do what you want it to do, but return '{}' i.e. an object without properties, because functions are not supported in JSON.
Your desired result is not even valid JSON. newFunction() without quotes is not a supported value (string, number, array, object, boolean, null).
Edit: you could try to return newfunction.toString() in your replacer, which should deliver your function's source as string. When converting the JSON back, you then must eval() this string to get the actual function.
#derpirscher provided a very good answer that will probably get more upvotes than this one, but this is my preferred answer:
Based on derpirscher's answer I decided it would be easier to make my own version of JSON.stringify that allows you to replace properties with your own source code, and changed the name of the module so that there is no naming conflict with JSON.
It's on my github account:
https://github.com/johnlarson/xerxes