TypeScript:在 PascalCase 和 camelCase 之间切换时出现奇怪的行为

TypeScript: Strange behaviour when switching between PascalCase and camelCase

我正在将 C# 桌面应用程序重构为 Angular/TypeScript Web 应用程序。

C# 应用程序中的所有 class 属性都使用 PascalCase,因此我认为,最好保留它。

这里有 2 个基本相同的 TypeScript 示例 class。数字 1 使用 PascalCase,数字 2 使用驼峰式:

//PascalCase
export class Info 
{
    public ManagedType:string;

    public ApiTemplate:string;
}

//camelCase
export class Info 
{
    public managedType:string;

    public apiTemplate:string;
}

这是奇怪的行为:

  1. 我从网络服务器加载 JSON 数据并创建上述信息 class 的数组。这似乎并不重要,如果 TypeScript class 使用 PascalCase 或 camelCase。到目前为止,还不错。

    this.Infos = await this.HttpClient.get<Info[]>(this.Url).toPromise<Info[]>();
    
  2. 当我将我的数组记录到控制台时,我可以看到,输出使用驼峰式命名的属性,无论信息 class 使用 PascalCase 还是驼峰式命名。有点奇怪,但到目前为止还不错。

  3. 现在这里对我来说变得很奇怪:当我使用 PascalCase 并过滤数组时,获取信息的一个特定实例 class , 结果总是 undefined/null.

  4. 当我使用 camelCase 并过滤数组时,为了获取 Info class 的一个特定实例,结果被找到并正确。

    //This doesn't work: Info is always undefinded, although the Array exists.
    let Info = Infos.filter(i => i.ManagedType == "Something" && i.ApiTemplate == "Something else")[0];
    
    //This works: Info is found 
    let Info = Infos.filter(i => i.managedType == "Something" && i.apiTemplate == "Something else")[0];
    

我的问题是:

为什么会这样?这是 TypeScript 问题还是 Angular 问题?

是否有必须遵守的不成文约定?

为什么 TypeScript 编译器不抛出错误或警告,即使用 PascalCase 可能无法正常工作?

Why is that so? Is that a TypeScript issue or is this an Angular issue?

都没有。问题的原因是来自您的网络服务器的 json 数据与您在打字稿中定义的 class 信息不完全相同 structure/format。

Is there an unwritten convention that I have to follow?

嗯,是的。在将它们转换为特定的 classes 之前,您应该手动测试并确保您确实获得了正确的数据结构。为澄清起见,您应该获取 json(HTTP 响应的主体),将其解析为通用对象的 JSON,然后测试它是否确实具有所有属性(具有相同的名称和types) 作为 class (信息),你将把它们投射到。然后去做。

更新:实际上有一种很酷的方法可以确定一个对象是否是 一个特定的类型并让打字稿知道这提供了强大的 assurnace/type 保护。 Typescript 具有称为 User-defined Typeguard functions 的功能,您可以在其中定义一个 return 为 true 或 false 的函数,如果 对象被测试为特定类型。

// user-defined type-guard function
function isInfo(obj: Object): obj is Info {
    if ('ManagedType' in obj && 'ApiTemplate' in obj) {
        return true;
    } else {
    // object does not have the required structure for Info class
        return false;
    }
}

// lets assume that jsonString is a string that comes from an
// http response body and contains json data. Parse it "blindly" to a generic object
let obj = JSON.parse(jsonString);

if (isInfo(obj)) {
    obj.ApiTemplate; // typescript in this scope knows that obj is of type Info
} else {
    // in this scope, typescript knows that obj is NOT of type Info
}

Why doesn't the TypeScript compiler throw an error or a warning, that using PascalCase may not work properly?

因为您在使用 this.HttpClient.get<Info[]>(this.Url).toPromise<Info[]>(); 时使用了隐式转换,所以您告诉打字稿‘嘿,我知道在运行时,服务器将发送一个 json 字符串,该字符串将被解析并将与 Info[](信息对象数组)绝对完全兼容。但实际上在运行时这不会发生,因为 属性 名称的区分大小写存在细微差别。 Typescript 不会在这里出错,因为你隐含地告诉它你知道你在做什么。

所以详细说明:

很明显,您在运行时转换一个 JSON 对象,该对象与隐式转换为的 Info class 定义不完全兼容。 json 数据实际上具有 属性 驼峰式命名,但您已使用 PascalName 定义了信息 class。看看这个例子:

//PascalCase
class Info 
{
    public ManagedType:string;

    public ApiTemplate:string;
}

let jsonString = `{
    "managedType": "1234asdf",
    "apiTemplate": "asdf1234"
}`;

// And HERE IS THE ISSUE. This does an implicit cast to Info object
// assuming that the JSON parsed object will strictly be the same as defined Info
// class. But that is not guaranteed. Typescript just assumes that you know
// what you are doing and what kind of data you will actually get in 
// runtime.
let obj: Info = JSON.parse(jsonString); 

上面示例的最后一行与此完全相同 'blind' casting/converting:

this.Infos = await this.HttpClient.get<Info[]>(this.Url).toPromise<Info[]>();

本质上你是在告诉 typescript 响应将是一个信息数组 classes 定义与 class 定义完全相同,但实际上在实际的 json 数据中它们是不,因此 JSON.parse() 将 return 一个具有 属性 名称的对象与它们在 json 字符串中的名称完全相同,在驼峰式而不是 PascalCase 中让打字稿假设。

// typescript just assumes that the obj will have PascalCase properties 
// and doesn't complain. but in reality this at runtime will not work,
// because the json properties in the json string are camelCase. Typescript
// can not know what data you will actually cast to this type in runtime.
// and cannot catch this error
console.log(`accessing Info.ManagedType property: ${obj.ManagedType}`);

// lets check at runtime all the actual properties names
// they will be in camelCase, just like in the jsonString.
Object.keys(obj).forEach(key => {
    console.log(`found property name: ${key}`);
});