网站搜索

reactjs - 将 C# Dictionary 传递到 TypeScript Map


我一直在尝试寻找在 TypeScript 中处理键值对的最佳方法,当 C# 后端返回一个字典对象时,但到目前为止我尝试的任何方法都没有按预期工作。

这是我的 C# 代码:

    var displayFileds = new Dictionary<string, string>();
    displayFileds.Add("A", "A Value");
    displayFileds.Add("B", "B Value");

这是 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)

我本以为在 console.log 中会看到相同的值,但它们完全不同,这是为什么?如果没有办法直接将对象反序列化为 Map,那么在 TypeScript 中处理字典的最佳方法是什么?

[ 编辑 ]

这是从后端获取日期的方式(我正在使用 Axios):

const { data } = await secureAxios.post<MyResponse> 
(ApiEndpoints.GetResponse, request)

以及响应类型:

export type MyResponse= {
  displayFields: Map<string,string>
}

正如你从上面的开发者工具输出中所看到的,我得到的不是类型所表示的 Map,而只是一个对象,这不是我想要的

答案#1

Axios 使用便利的泛型设计模式。你可以提供响应的类型,但在运行时不会强制执行——在类型安全性方面,它等同于类型断言。

你的 C# 后端将一个 Dictionary 序列化为以下 JSON 字符串:

{
   "A":"A Value",
   "B":"B Value"
}

它被 Axios 解析为 JS 对象:

{
   A: "A Value",
   B: "B Value"
}

这正是你从你的响应中记录下来的内容。

更一般地说,这意味着:

  • 如果你相信后端能够以正确的格式接收数据,你应该指定响应的类型
  • 一个常见的陷阱是将类指定为响应类型——这行不通,因为 JSON.parse 会返回一个普通对象,而不会建立原型链。

你现在有两个选项:

按原样使用返回的选项,指定正确的类型

正确的类型(匹配从 JSON.parse 实际返回的数据)是索引类型:

type DisplayFields = {
   [key: string]: string
}

这是习惯用法的 TS,并且将使你能够通过键访问值。

将结果转换为地图

如果你依赖在代码的其他部分使用 Map:

  • 在 Axios 请求中使用索引类型
  • 一旦收到回应,立即将其转换为地图。

注意:我不确定你的数据在后端序列化后的形状——它是仅包含 Map,还是在 displayFields 属性下包含 Map 的对象。请根据接收到的数据形状修改前端的返回类型。

答案#2

  • 你的 TypeScript type MyResponse 是错误的:

    • 默认情况下,Newtonsoft.Json(又名 JSON.NET)和 System.Text.Json 都会将 Dictionary<String,String> 序列化为具有字符串键和值的 JSON 对象。

      • 即 interface MyResponse { readonly [key: string]: string }
      • 默认情况下,Newtonsoft.Json(也称 JSON.NET)和 System.Text.Json 都会将 Dictionary<String,String> 序列化为具有字符串键和值的 JSON 对象。
      • 即 interface MyResponse { readonly [key: string]: string }

    • JSON 只能包含 对象字面量(即 JavaScript 的 object 和 Array 值的某种组合(分别使用 {} 和 [] 表示)),以及其他原始字面量(严格 限于 string 字面量、number 字面量,以及 true/false/null)。

      • 即,你 不能 调用 JavaScript 类的构造函数——也不能使用任何需要构造对象的对象/类型,因此在 JSON 中引用 Map 类型是非法的。
    • 但是你的 TypeScript type MyResponse 使用了 Map 对象,如上所述,这是不正确/错误/非法的。


可能的解决方案:

  • 如果字典键的集合是“静态的”,并且构成了您的 Web 服务的“契约”部分,那么您应该使用一个定义良好的 C# DTO 类,并让您的 JSON 序列化器为您处理它。

    • 这比拥有任意且未记录的 JSON 对象键要好得多,并且意味着你可以使用代码生成工具为任何平台(网页、TypeScript、Java、Rust,甚至 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;
      }
      
  • 如果你的 displayFields 始终 是动态的,无法使用任何类型的 静态 接口表示(也就是说,你 确实想要 发送带有 string 键和 string 值的任意 JSON object),那么可以使用如下方式:

    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;
    }
    

    并且在 TypeScript 中这样使用(假设你正在使用 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 );
    }
    
  • 也...

    • 在 JSON 中,对象属性(也称为键)应始终使用 pascalCase,而不是 TitleCase。
    • 仅使用 TypeScript 的 interface 类型来描述 JSON 响应。避免使用 type,因为那会更容易无意中使用非 JSON 安全的技术和类型。
    • 此外,TypeScript 接口应描述不可变对象:所有属性都应为 readonly,所有集合都应为 readonly T[] 等等。

      • 这是一个好主意,因为它可以防止在有两个或更多独立消费者使用相同响应对象的情况下出现问题——即这些消费者顺序处理同一个响应对象图实例时,而你不希望其中一个修改了收到的响应对象,从而破坏其他消费者。

        • TypeScript 的类型系统并不是 完全“可靠”的,而数据变更是遇到不可靠情况的一个简单途径。保持不可变性会让其他所有事情更容易。
        • 这是一个好主意,因为它可以防止在有两个或更多独立的响应对象使用者的情况下发生问题——当这些使用者按顺序处理相同的响应对象图实例时,你不希望其中一个修改接收到的响应对象,从而影响另一个使用者。
        • TypeScript 的类型系统并非完全“可靠”,而数据的变更是遇到不可靠情况的一个常见方式。保持事物不可变会让其他一切变得更容易。

    • 这也适用于 C#:请注意 DTO class 定义都是不可变的,使用构造函数通过参数验证进行初始化,并且所有属性都是只读的。

      • 只有在你打算从 C# 内部反序列化 JSON 时,类构造函数上的 [JsonConstructor] 和 [JsonProperty] 属性才是必要的。
      • 如果你想知道为什么会有这么多重复(与一个更简单的 可变 C# DTO 类相比),请责怪 C# 语言设计者让自定义构造函数变得如此繁琐——不过请考虑使用新的 C# record 类型,因为那可以减少很多繁琐操作,但会限制你在构造函数中执行验证逻辑的能力。