DEV Community

Andrew Nosenko
Andrew Nosenko

Posted on • Edited on

Grokking type conversion between TypeScript basic types, classes instances and object literals

Edited: I've now filed a related issue in the TypeScript repo:
A flag to make TypeScript more strict towards duck-typing classes.


A disclaimer: this post is coming from an OOP/C# person with a solid JavaScript background who's slowly learning TypeScript. If you're a TypeScript ninja, you should probably be already aware of these language specifics. I admit I should have thoroughly read the Handbook before starting coding with TypeScript, but who has time for that? :)


Let me start with a simple question, could you predict the output from the following TypeScript snippet?

let s1: String = "s"; 
console.log(s1 instanceof String)
console.log(typeof s1)
console.log(s1.constructor === String)
Enter fullscreen mode Exit fullscreen mode

Here's a playground link for your convenience, and the output is:

false 
"string" 
true 
Enter fullscreen mode Exit fullscreen mode

So, s1 is not an instance of String (despite typed so), its typeof type is "string" but its constructor is nevertheless String. Coming from C#, this doesn't make a lot of sense 🙂

Now, try predicting this output:

let s2: String = new String("s"); 
console.log(s2 instanceof String)
console.log(typeof s2)
console.log(s2.constructor === String)
Enter fullscreen mode Exit fullscreen mode

Here's a playground link, and the output is:

true 
"object" 
true 
Enter fullscreen mode Exit fullscreen mode

Now, s2 is an instance of String, its typeof type is now "object" but its constructor is still String.

Finally, this one (playground):

let s3: string = "s"; 
console.log(s3 instanceof String) // TypeScript error
console.log(typeof s3)
console.log(s3.constructor === String)
Enter fullscreen mode Exit fullscreen mode

Output:

false 
"string" 
true 
Enter fullscreen mode Exit fullscreen mode

This might be confusing, but it makes sense if we recall that TypeScript doesn't have any runtime type system (unlike C#). It just emits JavaScript, using whatever nascent type system the current ECMAScript version of JavaScript has.

For example, the generated JavaScript for the first code snippet looks like this (compiled with tsc -t ESNext code.ts):

let s1 = "s";
console.log(s1 instanceof String);
console.log(typeof s1);
console.log(s1.constructor === String);
Enter fullscreen mode Exit fullscreen mode

As a C# person, I might expect TypeScript to turn let s1: String = "s" into let s1 = new String("s") in JavaScript, because I declared s1 to be of a non-basic, class type String (rather than of a basic, primitive value type string).

Well, this is not how it works. Why? I asked this question on StackOverflow, and I got an excellent answer:

It is specifically a non-goal of the TypeScript language design to "add or rely on run-time type information in programs, or emit different code based on the results of the type system". So any suggestion or expectation of this sort should not be made.

To dig a bit deeper, the same relationship is true for classes and simple anonymous objects in TypeScript (playground):

interface IC {
    s: string;
}

class C implements IC {
    s: string;
    constructor(s: string) {
        this.s = s;
    }
}

const c1: C = new C("c1");
// true
console.log(c1 instanceof C); 

const c2: C = { s: "c2" };
// false
console.log(c2 instanceof C); 

const c3: IC = { s: "c3" };
// false
console.log((c3 as C) instanceof C) 
Enter fullscreen mode Exit fullscreen mode

This works the way it does because TypeScript is a "duck typing" language.

Note the as operator? This is a "type assertion", rather than some kind of type conversion that might affect the generated code. It will not magically turn an anonymous object into a instance of a class.

If the object is of type any or unknown, the as operator simply tells TypeScript that (to our best knowledge) the object has the expected set of properties and methods of the target type, to avoid compile-time errors. If that's not true, we'll most likely experience runtime errors.

If the object is anything other type than any or unknown, the as operator will make sure the target type is compile-time compatible.

If we want to verify this during runtime, we should be using "type guards" for that.

Thus, the lessons I've learnt are:

  • Reading "Advanced Types" from the Handbook is a must.

  • Leave your #C baggage behind the closed door. There is no added runtime type system in JavaScript code that TypeScript would emit, and it won't magically make instanceof work the way we might expect, coming from C#. A class object must be explicitly constructed with new MyClass() syntax for instanceof to work, and that's still a feature of JavaScript, not something specific to TypeScript.

  • Use primitive basic types (string, boolean, number, object) everywhere in TypeScript, and avoid using their class wrappers (String, Boolean, Number, Object), as a general rule.

  • If you really need to test whether a particular variable or a property contains a string (boolean, number), prop.constructor === String does work consistently for basic and wrapper types. When it is true, you can go one step further and check if typeof prop === "string", to tell whether it's a basic type or a wrapper class.

Note that prop.constructor === String may not work across realms (e.g., iframes), but prop.constructor.name === "String" will.

I hope the above makes sense. Feel free to leave a comment here or on my Twitter if you disagree 🙂

Top comments (0)