I was trying to find a best way to handle key:value pairs in TypeScript when the c# backend returns a dictionary object but anything I tried so for is not working as expected.
this is my c# code:
var displayFileds = new Dictionary<string, string>();
displayFileds.Add("A", "A Value");
displayFileds.Add("B", "B Value");
this is TypeScript
type MyResponse = {
displayFields: Map<string,string>
}
console.log(response.displayFields)
//sample of Map created in Typescript directly
let map = new Map([
["A", "A value"],
["B", "B value"]
]);
console.log(map)
I expected to see the same values for the console.log but they are absolutely different, but why? If there is no way to deserialize the object directly into a Map then what is the best way to handle Dictionaries in TypeScript?
[ Edit ]
This is how the date is being fetched from the backend(I'm using Axios):
const { data } = await secureAxios.post<MyResponse>
(ApiEndpoints.GetResponse, request)
and the response type:
export type MyResponse= {
displayFields: Map<string,string>
}
As you can see from the above devtools output I'm not getting back a Map<string,string> as a type suggests but rather just an object, which is not what I want
CodePudding user response:
- Your TypeScript
type MyResponseis wrong:By default, both
Newtonsoft.Json(aka JSON.NET) andSystem.Text.Jsonwill serializeDictionary<String,String>to a JSON object with string-keys and string-value.- i.e.
interface MyResponse { readonly [key: string]: string }
- i.e.
JSON can only contain object literals (i.e. some composition of JavaScript's
objectandArrayvalues (expressed using{}and[]respectively), and other primitive literals (strictly limited to juststringliterals,numberliterals, andtrue/false/null.- i.e. you cannot invoke a JavaScript class constructor function - nor use any object/type that requires a constructed object, so referencing the
Maptype in JSON is illegal.
- i.e. you cannot invoke a JavaScript class constructor function - nor use any object/type that requires a constructed object, so referencing the
But your TypeScript
type MyResponseuses theMapobject, which, as mentioned above, is incorrect/wrong/illegal.
Possible solutions:
If the set of dictionary keys is "static" and forms part of your web-service's "contract" then you should use a well-defined C# DTO
classand let your JSON serializer handle it for you.This is much better than having arbitrary and undocumented JSON object keys, and means you can use code-generation tools to automagically create web-service client-libraries for any platform out there (the web, TypeScript, Java, Rust, even COBOL).
MyResponse.cs
public class MyResponse { [JsonConstructor] public MyResponse( [JsonProperty("displayFields")] MyResponseFields displayFields ) { this.DisplayFields = displayFields ?? throw new ArgumentNullException(nameof(displayFields)); } [JsonProperty("displayFields")] public MyResponseFields DisplayFields { get; } } public class MyResponseFields { [JsonConstructor] public MyResponseFields( [JsonProperty("a")] String a, [JsonProperty("b")] String b ) { this.A = a; this.B = b; } [JsonProperty("a")] public String A { get; } [JsonProperty("a")] public String B { get; } }MyResponse.ts
interface MyResponseFields { readonly a: string; readonly a: string; } interface MyResponse { readonly displayFields: MyResponseFields; }
If your
displayFieldsis always dynamic and cannot be expressed using any kind of static interface (i.e. you really do want to send down arbitrary JSONobjectwithstringkeys andstringvalues), then use something like this:MyResponse.cs
public class MyResponse { [JsonConstructor] public MyResponse( [JsonProperty("displayFields")] IReadOnlyDictionary<String,String> displayFields ) { this.DisplayFields = displayFields ?? throw new ArgumentNullException(nameof(displayFields)); } [JsonProperty("displayFields")] public IReadOnlyDictionary<String,String> DisplayFields { get; } }MyResponse.ts
interface ReadonlyStringDictionaryObject { readonly [key: string]: string; } interface MyResponse { readonly displayFields: ReadonlyStringDictionaryObject; }And used like so in TypeScript (assuming you're using
fetch):async function getMyResponse(): MyResponse { const resp = fetch( '/my/service/endpoint' ); if( resp.status === 200 && resp.headers.get('Content-Type')?.startsWith('application/json') ) { const json = await resp.json(); if( isMyResponse( json ) ) { return json; } } throw new Error( "something went wrong" ); } function isMyResponse( obj: unknown ): obj is MyResponse { return ( typeof obj === 'object' && obj !== null && 'displayFields' in obj && ( typeof ( obj as any ).displayFields === 'object' ); ); } function convertMyResponseToMap( r: MyResponse ): Map<string,string> { return new Map( Object.entries( r.displayFields ) ); } async function doStuff() { const myResponse = await getMyResponse(); const asMap = convertMyResponseToMap( myResponse ); console.log( asMap ); }Also...
- In JSON, object-properties (aka keys) should always be
pascalCase, notTitleCase. - Only use TypeScript
interfacetypes to describe JSON responses. Avoid usingtypebecause that makes it easier to inadvertently use non-JSON-safe techniques and types. - Also, TypeScript interfaces should describe immutable objects: all properties should be
readonly, all collections should bereadonly T[]and so on.- This is a good idea because it prevents things breaking in cases where you have two or more separate consumers of the same response object - where those consumers process the same response object-graph instance sequentially, and you don't want one altering the received response object such that it would break the other consumer.
- TypeScript's type-system is not entirely "sound", and data-mutations are an easy way to encounter unsound situations. Keeping things immutable makes everything else easier.
- This is a good idea because it prevents things breaking in cases where you have two or more separate consumers of the same response object - where those consumers process the same response object-graph instance sequentially, and you don't want one altering the received response object such that it would break the other consumer.
- This extends to C# too: note how the DTO
classdefinitions are all immutable, using constructors to initialize themselves with argument validation, and all properties are read-only.- The
[JsonConstructor]and[JsonProperty]attributes on the class constructors is only needed if you intend to also deserialize the JSON from within C#. - If you're wondering why there's so much repetition (compared to a simpler mutable C# DTO class, blame the C# language designers for making custom constructors so tedious - however do consider using the new C#
recordtypes instead as that cuts down on a lot of tedium, but limits your ability to perform validation logic in the constructor.
- The
- In JSON, object-properties (aka keys) should always be
CodePudding user response:
Axios uses Convenience generic design pattern. You can provide the type of the response, but it is not enforced at runtime - it is equal to type assertion in terms of type safety.
Your C# backend serializes a Dictionary to following JSON string:
{
"A":"A Value",
"B":"B Value"
}
which is parsed by Axios to JS object:
{
A: "A Value",
B: "B Value"
}
This is precisely what you get logged from your response.
More generally, this means that:
- you shall specify type of the response if you trust backend to receive data in the right format
- a common trap is specifying a class as a response type - this won't work as
JSON.parsewill return a plain object, without establishing prototype chain.
You now have 2 options:
Use returned option as is, specifying the correct type
The correct type (matching the data actually returned from JSON.parse) is an indexed type:
type DisplayFields = {
[key: string]: string
}
This is idiomatic TS, and will give you the ability to access values by key.
Transform the result to Map
If you rely on having a Map in the rest of your code:
- use the indexed type for Axios request
- transform it to a Map as soon as you receive the response.
Note: I am not sure about the shape of your data serialized by backend - if it is only the Map or object containing the Map under the displayFields property. Modify the return type on frontend side according to shape of received data.
CodePudding user response:
You can achieve the same output as response.displayFields, by simply using Javascript object as:
const obj = {
A: "A value",
B: "B value"
}
and the same with typescript type definition can be written as:
const obj: Record<string, string> = {
A: "A value",
B: "B value"
}

