Skip to content

Instantly share code, notes, and snippets.

@lokshunhung
Last active January 22, 2021 09:44
Show Gist options
  • Save lokshunhung/7c350241f240c033f65144f2a25402f0 to your computer and use it in GitHub Desktop.
Save lokshunhung/7c350241f240c033f65144f2a25402f0 to your computer and use it in GitHub Desktop.
JS/TS Types - Sharing 20200122
# 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