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)
Here's a playground link for your convenience, and the output is:
false
"string"
true
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)
Here's a playground link, and the output is:
true
"object"
true
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)
Output:
false
"string"
true
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);
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)
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 withnew MyClass()
syntax forinstanceof
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 istrue
, you can go one step further and check iftypeof 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)