Last active
January 22, 2021 09:44
-
-
Save lokshunhung/7c350241f240c033f65144f2a25402f0 to your computer and use it in GitHub Desktop.
JS/TS Types - Sharing 20200122
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 1) JS Types | |
There are 2 types in JS: **primitives** and **objects**. | |
## 1.1) Primitives | |
- `undefined` | |
- `null` | |
- `boolean` | |
- `number` | |
- `string` | |
And additions introduced in newer versions of JS: | |
- `symbols` | |
- `bigint` | |
## 1.2) Objects | |
- `{}` ; `new Object()` | |
- `[]` ; `new Array()` | |
- `function () {}` ; `new Function()` | |
- `new Promise()` ; (and many more) | |
Things you should never use like: | |
- `new Boolean(0)`; `new Number("123")`; `new String(123)` | |
And newer stuff like: | |
- `class Animal {}` | |
- `new Map()` ; `new Set()` ; `new WeakMap()` ; `new WeakSet()` | |
--- | |
--- | |
--- | |
# 2) Operators for types | |
## 2.1) `typeof` operator | |
Usage: | |
`typeof <SOME_REFERENCE>` | |
→ returns a string describing the type | |
Example: | |
- `console.log( typeof "hi" )` | |
→ outputs `"string"` | |
- `var a = true; console.log( typeof a )` | |
→ outputs `"boolean"` | |
- `var b = 123; console.log( typeof 123 )` | |
→ outputs `"number"` | |
How is this operator useful: | |
- runtime type-checking (JS is dynamically typed language) | |
- `console.log( thisRefDoesNotExists )` | |
→ errors `thisRefDoesNotExists is not defined` | |
`console.log( typeof thisRefDoesNotExists )` | |
→ outputs `"undefined"` | |
Why this operator is flawed: | |
- `console.log( typeof null )` | |
→ outputs `"object"` | |
(this was a bug and has become a feature of the language) | |
- `console.log( typeof new Number("123") )` | |
→ outputs `"object"` | |
(no way to differentiate boxed type) | |
- `class A {}; console.log( typeof new A() )` | |
→ outputs `"object"` | |
(that's not very informative) | |
## 2.2) `instanceof` operator | |
Usage: | |
`<SOME_REFERENCE> instanceof <SOME_CONSTRUCTOR_REFERENCE>` | |
→ returns boolean indicating if `SOME_CONSTRUCTOR_REFERENCE` is part of `SOME_REFERENCE`'s prototype chain | |
### What is prototype chain? | |
This is how JS pretends it has OOP features. | |
Consider this Java code: | |
```java | |
class Animal { | |
String name; | |
Animal(String name) { | |
this.name = name; | |
} | |
void speak() { | |
System.out.println("I am " + name); | |
} | |
void getShortName() { | |
return this.name.substring(0, 3); | |
} | |
} | |
class Dog extends Animal { | |
Dog(String name) { | |
super(name); | |
} | |
@Override | |
void speak() { | |
System.out.println("I am " + name + ", woof"); | |
} | |
void handHand() { | |
System.out.println("👋🏼"); | |
} | |
} | |
new Dog("Bobby") | |
``` | |
JS can do this instead: | |
```js | |
function Animal(name) { | |
this.name = name; | |
} | |
Animal.prototype.speak = function () { | |
console.log("I am " + this.name); | |
}; | |
Animal.prototype.getShortName = function () { | |
return this.name.substring(0, 3); | |
}; | |
function Dog(name) { | |
this.name = name; | |
} | |
Dog.prototype = new Animal(); | |
Dog.prototype.constructor = Dog; | |
Dog.prototype.speak = function () { | |
console.log("I am " + this.name + ", woof"); | |
}; | |
Dog.prototype.handHand = function () { | |
console.log("👋🏼"); | |
}; | |
``` | |
But that's clumsy. Let's use ES6 classes. | |
```js | |
class Animal { | |
constructor(name) { | |
this.name = name; | |
} | |
speak() { | |
console.log(`I am ${this.name}`); | |
} | |
getShortName() { | |
return this.name.substring(0, 3); | |
} | |
} | |
class Dog extends Animal { | |
constructor(name) { | |
super(name); | |
} | |
speak() { | |
console.log(`I am ${this.name}, woof`); | |
} | |
handHand() { | |
console.log("👋🏼"); | |
} | |
} | |
``` | |
Back to `instanceof` operator. | |
Example: `var d = new Dog("Bobby"); var a = new Animal("Root")` | |
- `console.log( d instanceof Dog, d instanceof Animal )` | |
→ outputs `true true` | |
- `console.log( a instanceof Dog, a instanceof Animal )` | |
→ outputs `false true` | |
How is this operator useful: | |
```js | |
try { | |
callAPI(); | |
} catch (error) { | |
if (error instanceof APIError) { | |
handleError(error); | |
} else { | |
throw error; | |
} | |
} | |
``` | |
Why is this operator flawed: | |
- `instanceof` checks for referential equality of prototypes | |
So it sometimes behaves unexpectedly. | |
```js | |
var elementOrNull = tryFindElementInIFrame(); | |
console.log(elementOrNull instanceof Element); | |
``` | |
This always outputs `false`, because `window.Element` is a different reference from `iframe.contentWindow.Element` | |
- This is also fairly common when a library is bundled more than once (with different versions) in your build. | |
e.g. Having 2 versions of `core-fe` and `axios` | |
--- | |
--- | |
--- | |
# JS and types | |
The language is dynamically typed and is hard to scale when team size grows. | |
Runtime type-checking: | |
- has performance penalty, | |
- is cumbersome, and | |
- creates a bad developer experience (feedback iteration cycle is slow) | |
There are many attempts on fixing JS, replacing JS, compiling to JS. | |
TS seems to be the champion, for now. | |
--- | |
--- | |
--- | |
# 3) TS | |
Excellent interoperability with JS, can 100% leverage current JS ecosystem. | |
Looks like C# (because the lead architect Anders Hejlsberg is one of the core developers). | |
Easy to get started - by putting types on JS with tape, but sacrifices some safety that are guaranteed in other compiled languages. | |
Think: rust but implicitly with `unsafe { ... }`, probably everywhere. | |
Note that type-checking and code-emitting are two separate phases. | |
## 3.1) Typing objects in TS | |
In JS, objects are dynamically typed (think `HashMap` in Java). | |
```js | |
const obj = { | |
prop1: "a", | |
prop2: 1, | |
}; | |
obj.prop3 = true; // <-- this is valid during runtime | |
``` | |
TS allows types to be defined. | |
```ts | |
interface Obj { | |
prop1: string; | |
prop2: number; | |
} | |
const obj: Obj = { | |
prop1: "a", | |
prop2: 1, | |
}; | |
obj.prop3 = true; // <-- TS throws error during "type-checking" | |
``` | |
Both code snippets are equivalent after transpilation. | |
- type-checking: tries to reports error according to type signatures | |
- code-emitting: erases type signatures, outputs code that can be run in browser/node/etc | |
## 3.2) Typing functions in TS | |
In many languages, function signatures are the composite of argument type and return type. | |
```ts | |
function isSimilar(s1: string, s2: string): boolean { | |
return s1.toUpperCase() === s2.toUpperCase(); | |
} | |
``` | |
The type signature can be created without sticking it on the implementation. | |
```ts | |
type IsSimilar = (s1: string, s2: string) => boolean; | |
const isSimilar: IsSimilar = /* implementation */; | |
``` | |
It can also be written like this (think function is an object, but callable). | |
```ts | |
type IsSimilar = { (s1: string, s2: string): boolean }; | |
const isSimilar: IsSimilar = /* implementation */; | |
``` | |
## 3.3) Typing classes in TS | |
Easy. | |
```ts | |
class Animal { | |
name: string; | |
constructor(name: string) { | |
this.name = name; | |
} | |
} | |
class Dog extends Animal { | |
constructor(name: string) { | |
super(name); | |
} | |
} | |
const a: Animal = new Dog("Bobby"); | |
``` | |
# 4) Idiosyncrasies of TS | |
> "Duck typing": | |
> If it walks like a duck and it quacks like a duck, then it must be a duck. | |
Almost everything is structurally typed. | |
Nominal typing is achievable, but they are cumbersome and rarely used in practice. | |
## 4.1) Classes without nominal typing | |
The following works because `r` is assignable to `{width: number}`, | |
therefore fulfills the requirement to be a `Square`. | |
```ts | |
interface Square { | |
width: number; | |
} | |
interface Rectangle { | |
width: number; | |
height: number; | |
} | |
function getSquareArea(square: Square): number { | |
return square.width * square.width; | |
} | |
const r: Rectangle = {width: 10, height: 20}; | |
getSquareArea(r); | |
``` | |
This can get confusing: | |
```ts | |
class Animal { | |
name: string; | |
} | |
class Cat extends Animal { | |
ownerName: string; | |
} | |
class Dog extends Animal { | |
ownerName: string; | |
} | |
const bobby: Cat = new Dog(); // <-- passes type-checking ??? | |
console.log(bobby instanceof Cat); // <-- outputs `false` ??? | |
const mittens: Cat = {name: "mittens", ownerName: "Lok"}; | |
console.log(mittens instanceof Cat); // <-- outputs `false` ??? | |
``` | |
If we take a look at the underlying types, they should be: | |
```fs | |
interface Cat { | |
name: string; | |
ownerName: string; | |
} | |
interface Dog { | |
name: string; | |
ownerName: string; | |
} | |
``` | |
In other words, these types are equivalent; | |
so for "bobby", `new Dog()` is assignable to the type `Cat`; | |
and for "mittens", the object literal is assignable to the type `Cat`. | |
The prototype chain of `bobby` is `Dog -> Animal -> Object`. | |
The prototype chain of `mittens` is `Object`. | |
Both `bobby instanceof Dog` and `mittens instanceof Cat` returns `false` | |
because `instanceof`'s RHS is not found as the constructor in the prototype chain of `instanceof`'s LHS. | |
For developers coming from JS, this is kind of the expected behaviour. | |
But for developers jumping into TS from other languages, this is rather confusing. | |
## 4.2) Types are erased after transpilation | |
Consider the Dog-Animal example rewritten using composition. | |
```ts | |
interface Animal { | |
name: string; | |
} | |
interface HasOwner { | |
ownerName: string; | |
} | |
class Dog implements Animal, HasOwner { | |
constructor(public name: string, public ownerName: string) {} | |
} | |
``` | |
The following code will throw both during type-checking and runtime. | |
```ts | |
const d = new Dog("Bobby", "Lok"); | |
console.log(d instanceof Animal); | |
// ^^^^^^ | |
// 'Animal' only refers to a type, but is being used as a value here. ts(2693) | |
// ...and during runtime: | |
// "Animal is not defined" | |
``` | |
TS introduces the concept of "type-space" and "value-space". | |
Anything in TS not understood by JS runtimes (browsers/node/etc) must be transpiled or removed. | |
`interface` is a TS thing, and exists in "type-space" only. | |
It does not exists in JS, and maps to nothing in "value-space". | |
## 4.3) Excess property checking | |
Assuming we have: | |
```ts | |
interface Point2D { | |
x: number; | |
y: number; | |
} | |
function printPoint2D(point: Point2D): void { | |
console.log(`(${point.x}, ${point.y})`); | |
} | |
``` | |
The type-checker reports this as an error: | |
```ts | |
const point2D: Point2D = {x: 10, y: 20, z: 30}; | |
// ^^^^^ | |
// Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point2D'. | |
// Object literal may only specify known properties, and 'z' does not exist in type 'Point2D'. ts(2322) | |
``` | |
But has no problem with this | |
```ts | |
const p = {x: 10, y: 20, z: 30}; | |
printPoint2D(p); // <-- why does this pass ??? | |
``` | |
Because "excess property checking" only works on "object literals", but not "references". When using a "reference", only assignability is checked. | |
For TS to report the error, use this instead: | |
```ts | |
printPoint2D({x: 10, y: 20, z: 30}); | |
// ^^^^^ | |
// Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point2D'. | |
// Object literal may only specify known properties, and 'z' does not exist in type 'Point2D'. ts(2322) | |
``` | |
p.s. If you look closely, the error message mentioned "**_Object literal_** may only specify...", so this is working as designed and totally not a bug. | |
--- | |
--- | |
--- | |
# 5) Making TS safe | |
Behaviour of TS depends on compiler flags, usually specified in `tsconfig.json`. | |
Turn these `compilerOptions` on: | |
- `"allowJs": false` | |
- `"strict": true` | |
- `"noFallthroughCasesInSwitch": true` | |
- `"noPropertyAccessFromIndexSignature": true` | |
- `"noUncheckedIndexAccess": true` | |
- `"importsNotUsedAsValues": "error"` | |
- `"forceConsistentCasingInFileNames": true` | |
TS cannot check everything, so using additional static analyzers such as linters are common. | |
Using `eslint` with this `.eslintrc.json`: | |
```jsonc | |
{ | |
"extends": ["plugin:@pinnacle0/baseline"], | |
"overrides": [ | |
{ | |
"files": ["**/*.test.ts", "**/*.test.tsx"], | |
"extends": ["plugin:@pinnacle0/jest"] | |
} | |
] | |
} | |
``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment