Convert JSON with "string enum" properties to interface - json

In our project, we are using string enum, to send the data from the BE to the client. Now the issue is, that when we mock the data, it contains the "enum strings" in the mock json files.
These mock json files are then imported like this: import testJson from '.test.json'; ('resolveJsonModule' is enabled in tsconfig), which gives them a type 'based on the static JSON shape.' (See: Resolve JSON Module).
Now the type which is generated for that enum property is string and not enum, so when I try to cast the json to the interface type, it throws me an error: Type 'string' is not assignable to type 'testEnum'.
Stackblitz example:
const testJSON = {
name: 'hello World',
enumProp: "MYTESTENUM_2",
};
enum testEnum {
MYTESTENUM = "MYTESTENUM",
MYTESTENUM_2 = "MYTESTENUM_2",
}
interface testinterface {
name: string;
enumProp: testEnum;
}
//const castJsonFile: testinterface = testJSON; // not compiling because the "string enums" cannot be converted
const castJsonFile: testinterface = testJSON as any; // any cast is needed, to get it compiling, but looses all type safety
// Write TypeScript code!
const appDiv: HTMLElement = document.getElementById('app');
appDiv.innerHTML = (castJsonFile.enumProp === testEnum.MYTESTENUM_2) as any as string;
Link: https://stackblitz.com/edit/typescript-fjy8zs?file=index.ts
If I would not use "string enums" it would work fine, but this is a change which cannot be done.
Is there a way to tell typescript to treat the "string enums" as enums if he finds such a property in a JSON file?

Related

typescript/angular: can't iterate over Map parsed from json

I'm new to both typescript and angular (coming from quite solid Java/kotlin background).
Wrote a class:
export interface GeoData {
allregions: Map<string, string>;
}
to parse this json:
{"allregions":{"whatever":"WHT","a region":"ARE","something":"SMT"}
The json is correctly read from a file using HttpClient.get() and I can see the correct content in the variable using debug. also, this code:
console.log(data.allregions["whatever"])
correctly prints WHT.
unfortunately, this:
data.allregions.forEach((value: string, key: string) => {
console.log(key, value);
});
throws data.allregions.forEach is not a function
also this:
console.log(data.allregions.size)
prints undefined
and this:
console.log(data.allregions.entries.lenght)
throws data.allregions.entries is undefined
what is going on here?
I see you are applying forEach on a object. Combine Object.keys() and forEach()
var data = {"allregions":{"whatever":"WHT","a region":"ARE","something":"SMT"}}
Object.keys(data.allregions).forEach(function(key) {
console.log(`key: ${key}, value:${data.allregions[key]}`);
});

Can't get Nested JSON Object Property Typescript

I want to get CAD value from https://api.exchangeratesapi.io/latest
I have already used so many types of code, and it says that "TypeError: Cannot read property 'CAD' of undefined"
Really need your help, thank you very much.
It outputted all the Currencies if I console this code
((this.state.data as any).rates)
but when i want to get CAD currencies, it says the error
I have tried these codes :
((this.state.data as any).rates as any).CAD
(this.state.data as any)["Rates"]["CAD"];
(this.state.data as any)["Rates"].CAD;
The way I get the data is
interface IState {
data?: object | null;
isCurrency?: boolean;
Currency?: string;
Rate?: number;
}
export default class Header extends Component<{}, IState> {
service: UserService = new UserService();
state = {
isCurrency: false,
Currency: "USD",
Rate: 1,
data: [] = []
};
async componentDidMount() {
let result = await this.service.getAllCurrency();
this.setState({
data: (result as Pick<IState, keyof IState>).data
});
console.log(result);
}
}
1.4591 (Based on the latest API)
You should create a type for your data. Because it's coming from an external source, typescript cannot infer that. Then parse your JSON and cast it to that type.
// Create a type for the expernal data.
interface Data {
rates: {
[currency: string]: number
}
base: string
date: string
}
// Result of `JSON.parse()` will be `any`, since typescript can't parse it.
const untypedData = JSON.parse(`{
"rates": {
"CAD": 1.4591,
"HKD": 8.6851,
"ISK": 135.9,
"PHP": 56.797,
"DKK": 7.4648
},
"base": "EUR",
"date": "2019-07-25"
}`)
// Cast the untyped JSON to the type you expect it to be.
const data: Data = untypedData
// Use the data according to it's type.
alert(data.rates.CAD)
Working demo on typescript playground

Casting an HTTP response from JSON to interface not behaving as expected

I am making a get request to a server from an angular client. The service in which I am making the calls needs to return an observable with an array of objects. The problem is that typescript doesn't seem to allow me to cast the JSON to my interface.
This is the JSON object the server is sending as a response:
export interface Message {
title: string;
body: string;
}
This is the interface I want to cast the body to. There body should contain an array of these objects:
export interface ICommonGameCard {
id: string;
pov: POVType;
title: string;
image_pair?: ICommonImagePair;
best_time_solo: number[];
best_time_online: number[];
}
export enum POVType{Simple, Free};
This the the service making the requests:
public getGameCards(povType: POVType): Observable<ICommonGameCard[]> {
return this.http.get<ICommonGameCard[]>(this.BASE_URL + this.GET_ALL_CARDS_URL)
.pipe(
map((res: Response) => return <ICommonGameCard>res.json().body),
catchError(this.handleError<Message>("getUsernameValidation")),
);
}
Obviously, this is not working. I am trying to cast the response's JSON to the message Interface, and then access the message's interface body where there is an array of ICommonGameCard.
I am getting this error:
[ts]
Argument of type 'OperatorFunction<Response, ICommonGameCard>' is not assignable to parameter of type 'OperatorFunction<ICommonGameCard[], ICommonGameCard>'.
Type 'Response' is missing the following properties from type 'ICommonGameCard[]': length, pop, push, concat, and 26 more. [2345]
What exactly is wrong with my syntax?
export interface Message {
title: string;
body: ICommonGameCard[];
}
public getGameCards(povType: POVType): Observable<ICommonGameCard[]> {
return this.http.get<Message>(this.BASE_URL + this.GET_ALL_CARDS_URL)
.pipe(
map((message: Message) => message.body),
catchError(this.handleError<Message>("getUsernameValidation"))
);
}

Proper way to thoroughly write a JSON object typing

In here: https://hackernoon.com/import-json-into-typescript-8d465beded79, I read it is possible to import JSON objects to my TypeScript project. I did that with the following JSON:
{
"defaultLanguage": "en",
"languageMap": {
"en": "English",
"pl": "Polish",
"de": "German"
}
}
I now want to make sure that any future changes to this file do not break my application so I introduced a following interface to the imported object:
export default interface IConfig {
defaultLanguage: string;
languageMap: ILanguage
}
interface ILanguage {
[language: string]: string
}
I have changed the typings.d.ts file:
declare module "*.json" {
const value: any;
export default value;
}
And imported to my Angular component:
import IConfig from './IConfig'; // Tried to put IConfig
// as a type in multiple places.
import * as config from './config.json';
(...)
private languageMap = config.languageMap; // in this line my Visual Studio Code
// underlines languageMap and shows
// the following error:
[ts] Property 'languageMap' does not exist on type 'typeof "*.json"'.
And below:
any
I am wondering if there is a way not to use (<any>config).languageMap but to include my IConfig interface, as suggested in the link above.
The module *.json module defines a wilds card module that will match any file ending with json and will type the json value as any. You can define a more specific module for config.json.
You can place a config.json.d.ts along side the config.json file describing the json:
//config.json.d.ts
interface IConfig {
defaultLanguage: string;
languageMap: ILanguage
}
interface ILanguage {
[language: string]: string
}
declare const value: IConfig;
export = value;
// usage.ts
import * as config from './config.json';
var languageMap = config.defaultLanguage; // All ok
Or if you use a module system taht does not support export=value you can use this definitions:
//config.json.d.ts
export let defaultLanguage: string;
export let languageMap: ILanguage
interface ILanguage {
[language: string]: string
}

Deserializing json in Angular 2/4 using HttpClientModule

So I'm getting the following JSON structure from my asp.net core api:
{
"contentType": null,
"serializerSettings": null,
"statusCode": null,
"value": {
"productName": "Test",
"shortDescription": "Test 123",
"imageUri": "https://bla.com/bla",
"productCode": null,
"continuationToken": null
}
}
I have the following typescript function that invokes the API to get the above response:
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; //not working...
console.log("DATA: " + data);
console.log("DATA: " + data['value']);
return data;
});
}
ProductVM:
export interface ProductVM {
productName: string;
shortDescription: string;
imageUri: string;
productCode: string;
continuationToken: string;
}
My problem is that I can't deserialize it to ProductVM. The console logs just produce [object Object]
How can I actually map the contents of the value in my json response to a ProductVM object?
Is it wrong to say that data is a ProductVM in the map function? I have tried lots of different combinations but I cannot get it to work!
I'm unsure whether I can somehow automatically tell angular to map the value array in the json response to a ProductVM object or if I should provide a constructor to the ProductVM class (it's an interface right now), and extract the specific values in the json manually?
The data object in the map method chained to http is considered a Object typed object. This type does not have the value member that you need to access and therefore, the type checker is not happy with it.
Objects that are typed (that are not any) can only be assigned to untyped objects or objects of the exact same type. Here, your data is of type Object and cannot be assigned to another object of type ProductVM.
One solution to bypass type checking is to cast your data object to a any untyped object. This will allow access to any method or member just like plain old Javascript.
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: any) => this.externalProduct = data.value);
}
Another solution is to change your API so that data can deliver its content with data.json(). That way, you won't have to bypass type checking since the json() method returns an untyped value.
Be carefull though as your any object wil not have methods of the ProductVM if you ever add them in the future. You will need to manually create an instance with new ProductVM() and Object.assign on it to gain access to the methods.
From angular documentation: Typechecking http response
You have to set the type of returned data when using new httpClient ( since angular 4.3 ) => this.http.get<ProductVM>(...
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get<ProductVM>("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; // should be allowed by typescript now
return data;
});
}
thus typescript should leave you in peace
Have you tried to replace
this.externalProduct = data;
with
this.externalProduct = data.json();
Hope it helps
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map(data => {
this.externalProduct = <ProductVM>data;
console.log("DATA: " + this.externalProduct);
return data;
});
}
So, first we convert the response into a JSON.
I store it into response just to make it cleaner. Then, we have to navigate to value, because in your data value is the object that corresponds to ProductVM.
I would do it like this though:
Service
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get(`api/product?productCode=${code}`)
.map(data => <ProductVM>data)
.catch((error: any) => Observable.throw(error.json().error || 'Server error'));
}
Component
this.subscription = this.myService.getProductExternal(code).subscribe(
product => this.externalProduct = product,
error => console.warn(error)
);
I used this approach in a client which uses the method
HttpClient.get<GENERIC>(...).
Now it is working. Anyway, I do not understand, why I do not receive a type of T back from the http client, if I don't use the solution provided in the answer above.
Here is the client:
// get
get<T>(url: string, params?: [{key: string, value: string}]): Observable<T> {
var requestParams = new HttpParams()
if (params != undefined) {
for (var kvp of params) {
params.push(kvp);
}
}
return this.httpClient.get<T>(url, {
observe: 'body',
headers: this.authHeaders,
params: requestParams
}).pipe(
map(
res => <T>res
)
);
}