arraysangulartypescriptcamelcasingpascalcasing

TypeScript: Strange behaviour when switching between PascalCase and camelCase


I am refactoring a C# desktop application to an Angular/TypeScript web application.

All class properties inside the C# application use PascalCase, so I thought, it would be a good idea to just keep that.

Here are 2 examples of basically the same TypeScript class. Number 1 uses PascalCase, number 2 uses camelCase:

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

    public ApiTemplate:string;
}

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

    public apiTemplate:string;
}

Here is the strange behaviour:

  1. I load JSON data from the webserver and create an array of the above Info class. It doesn't seem to matter, if the TypeScript class uses PascalCase or camelCase. So far, so good.

    this.Infos = await this.HttpClient.get<Info[]>(this.Url).toPromise<Info[]>();
    
  2. When I log my array to the console, I can see, that the output uses camelCase for the properties, no matter if the Info class uses PascalCase or camelCase. A little strange, but so far so good.

  3. Now here it became strange to me: When I use PascalCase and filter the array, to get one specific instance of the Info class, the result is always undefined/null.

  4. When I use camelCase and filter the array, to get one specific instance of the Info class, the result is found and correct.

    //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];
    

My questions are:

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

Is there an unwritten convention that I have to follow?

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


Solution

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

    Neither. The cause of the issue is that the json data that comes from your webserver is not in the exactly same structure/format that you have defined the class Info in typescript.

    Is there an unwritten convention that I have to follow?

    Well, yes there is. You should manually test and make sure that you actually get the correct data structures before casting them to specific classes. To clarify, you should take the json (body of the HTTP response), parse it as JSON to a generic object, and then test it if it actually has exactly all the properties (with same name and types) as the class (Info) that you are about to cast them to. And then do it.

    UPDATE: actually there is a cool way to determine if an object is a particular type and let typescript know about this providing a strong assurnace/type guarding. Typescript has this feature called User-defined Typeguard functions, where you define a function that returns true or false if an object is tested to be of a particular type.

    // 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?

    Because you are using implicit cast when you are using the this.HttpClient.get<Info[]>(this.Url).toPromise<Info[]>(); you telling typescript that 'hey I know that in runtime, the server will send a json string that will be parsed and will be absolutely exactly compatible with the Info[] (an array of Info Objects). But actually in runtime this doesn't happen because there is a small difference in the case sensitivity of the property names. Typescript will not error here because you implicitly told it that you know what you are doing.

    So to elabarate:

    It is obvious that you are converting at runtime a JSON object that is not completely compatible with the Info class definition that you implicitly cast it to. The json data actually has the property names with camelCase, but you have defined the Info class with PascalName. Take a look at this example:

    //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); 
    
    

    The last line of the above example does the same exact 'blind' casting/converting that this does:

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

    Essentially you are telling typescript that the response will be an Array of Info classes defined exactly as the class definition, but in reality in the actual json data they are not, the differ therefore the JSON.parse() will return an object that has the property names exactly as they are in the json string, in camelCase instead of PascalCase that you let typescript to assume.

    // 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}`);
    });