Reputation: 20364
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.
Upvotes: 0
Views: 1495
Reputation: 329608
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!
Upvotes: 2
Reputation: 185140
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')
Upvotes: 2