Grokking type conversion between TypeScript basic types, classes instances and object literals
Tags: typescript, javascript, types
Published: 2020-11-25 01:22:24

**Edited**: I've now filed a related issue in the TypeScript repo: 
[*A flag to make TypeScript more strict towards duck-typing classes*](https://github.com/microsoft/TypeScript/issues/42698).
<hr>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](https://www.typescriptlang.org/docs/handbook/) before starting coding with TypeScript, but who has time for that? :)

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

```TypeScript
let s1: String = "s"; 
console.log(s1 instanceof String)
console.log(typeof s1)
console.log(s1.constructor === String)
```
Here's a [playground link](https://www.typescriptlang.org/play?#code/DYUwLgBAzgjAXBAymATgSwHYHMIF4IBEUBA3BAFADGA9hlNaAHTDVYAUsEmUYAhhpRDUAZklSYsASiq16TFuzABPAA5DRsaTToMQzVhxiNtPFAFdKYaijy58ydNmlA) for your convenience, and the output is:

```text
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:

```TypeScript
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](https://www.typescriptlang.org/play?#code/DYUwLgBAzgTAXBAymATgSwHYHMIF4IYgDuSqmWAFAERRUCUA3BAFADGA9hlO6AHTDtKsCJihgAhhlYh2AM1LpsdNp258BlMAE8ADjPmxlHLjxD9BFWL2NiUAV1Zh2KPLnzJFWZUA), and the output is: 

```text
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](https://www.typescriptlang.org/play?#code/DYUwLgBAzgzAXNMAnAlgOwOYQLwQERR4DcEAUAMYD2aUloAdMJRgBSwTpRgCGa5IlAGYQAysnQYAlBAD0MiCCRJKSCtVoMmrMAE8ADgOGxJamnRCNmbGPSo1kAV3JgVObLjGpMJoA)):

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

Output:

```text
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`):

```JavaScript
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](https://stackoverflow.com/q/64941184/1768303) on StackOverflow, and I got an [excellent answer](https://stackoverflow.com/a/64948481/1768303):

> It is specifically [a non-goal of the TypeScript language design](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals) 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](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJIGFkG8BQz-IDOAXEWFKAOYDcOAvjjggDZyGHKbAC2ADsxG4RwHDNjwESZCiBoT8CAPYhC5AK4IwiqAAopqmZQCU4gmeRgAFsEIA6DgF4itMwwZNlq5AgCMpTE4gEADunDoARL7hRrRKKooCtsyKlDq+yKCqcCBIijCcMcgA9EUWUGoQjHFeCABM-shOWESkkbXhyHSxngkQSSlptRkqYNm5+eiFJcjwzISVHiPeAMykYk0tyJHLHV2LhL39qWnLyGwFw1k5EHkX07PzjEA)):

```TypeScript
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](https://www.typescriptlang.org/docs/handbook/interfaces.html). 

Note the `as` operator? This is a ["type *assertion*"](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions), 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"](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) for that. 

Thus, the lessons I've learnt are:

- Reading ["Advanced Types"](https://www.typescriptlang.org/docs/handbook/advanced-types.html) 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](https://twitter.com/noseratio) if you disagree 🙂