p:: Programming Language, JavaScript f:: CLI, Web Apps
Typing in JavaScript
Javascript Types
Six primitive data types in JavaScript
Boolean
Number
BigInt
String
Symbol
undefined
andnull
1. Boolean
either true
or false
typeof true; // "boolean"
In JavaScript, some values of different types are considered “falsy”, which means if you were to cast the value into a Boolean
using two NOT operators or with the Boolean()
built-in function, it would become false
, where every other value would become true
.
2. Number
Number
s in JavaScript are stored as floats, which means you use the same type for whole integers and decimals. JavaScript also has values for Infinity
, -Infinity
, and NaN
, or “Not a Number”.
let imaginaryNumber = Math.sqrt(-1); // NaN
typeof imaginaryNumber; // "number"
imaginaryNumber === NaN; // false
Number.isNaN(imaginaryNumber); // true
Boolean(0); // false
Boolean(NaN) // false
3. BigInt
very large integers
typeof 1337n; // "bigint"
1337n === 1337; // false
Boolean(0n); // false
4. String
let fruitName = "Banana";
typeof fruitName; // "string"
Boolean(""); // false
5. Symbol
unique and immutable
let symbol1 = Symbol("Apple");
let symbol2 = Symbol("Apple");
symbol1 === symbol2; // false
typeof symbol1; // "symbol"
6. undefined
& null
undefined
represents uninitialized. null
represents the absence of value.
typeof undefined; // "undefined"
let nullish = null;
typeof nullish; // "object"
nullish === null; //true
Boolean(undefined); // false
Boolean(null); // false
JavaScript also has structural types, which means we can construct different shapes of the primitive types to represent more complex data structures.
Object
All of the other types we’ve looked at so far can be directly compared by value, but objects (and object-like values) are compared by reference. This means two objects that have the same shape aren’t considered equal.
let car = {
wheels: 4,
color: "red",
};
let car2 = {
wheels: 4,
color: "red",
};
car === car2; // false
let carClone = car;
car === carClone; // true
typeof car; // "object"
Array
Array
s store data, but use Number
s instead of String
s for indexing properties. This means that the data stored in Array
s is ordered.
let myArray = [1, 2, 3];
typeof myArray; // "object"
Array.isArray(myArray); // true
Function
typeof function () {}; // "function"
typeof (() => {}); // "function
Classes
class Car {
constructor(wheels, color) {
this.wheels = wheels;
this.color = color;
}
}
typeof Car; // "function"
let motorcycle = new Car(2, "black");
typeof motorcycle; // "object"
motorcycle instanceof Car; // true
Special TypeScript Types
any
and unknown
types
any
gives you the greatest flexibility, but absolutely no type safety, so it’s best to avoid any
unless it is absolutely necessary.
we can convince TypeScript that an unknown
or any
value actually has a more specific type by using a process called type narrowing. This involves doing runtime checks which either prove that a value is a specific type or prove that it is not a specific type.
const unknownNumber: unknown = 27;
let theAnswer: number = 0;
if (typeof unknownNumber === "number") {
theAnswer = 15 + unknownNumber;
}
As a general rule of thumb, prefer unknown
over any
and use type narrowing to determine a more accurate type.
Interfaces
interface EdibleThing {
name: string;
color: string;
}
interface Fruit extends EdibleThing {
sweetness: number;
}
const apple: Fruit = { name: "apple", color: "red", sweetness: 80 };
Indexable Types
We can mix index signatures with regular property signatures, so long as the key and value types match.
interface Fruit {
[key: string]: string;
name: string;
}
let apple: Fruit = {
name: "Apple",
ripeness: "overripe",
};
Enums
enum Seasons {
winter,
spring,
summer,
autumn,
}
function seasonsGreetings(season: Seasons) {
if ((season = Seasons.winter)) return "⛄️";
// ...
}
const greeting = seasonsGreetings(Seasons.winter);
Notice that we are able to use Seasons
as both a type and a value.
Tuple
Tuples are fixed-length arrays. We can tell TypeScript how many items are in the array, and what the type of each item is.
function simpleUseState(
initialState: string,
): [string, (newState: string) => void] {
// The rest of the implementation goes here.
}
TypeScript will never infer an array of items to be a tuple, even if the items are of different types.
Void and Never
void
void
represents the absence of any type. It’s often used to indicate that a function doesn’t return anything.
function performRequest(requestCallback: () => void) {
// implementation goes here
}
never
never
represents a value that can never occur. If we every try to access or modify a never
variable, TypeScript will give us a warning.
function exception(): never {
throw new Error("Something terrible has happened");
}
const output = exception();
// const output: never;
function loopForever(): never {
while (true) {}
}
const loopOutput = loopForever();
// const loopOutput: never;
type
aliases
type StringTree = {
value: string;
left?: StringTree;
right?: StringTree;
};
let myStringTree: StringTree = getStringTree();
myStringTree.value; // string
myStringTree?.left?.right?.left?.value; // string | undefined
Interface or type
?
Interfaces support extension using the extends
keyword, which allows an Interface to adopt all of the properties of another Interface. Because of this, Interfaces are most useful when you have hierarchies of type annotations, with one extending from another.
type
aliases, on the other hand, can represent any type, including functions and Interfaces!
Union Types
interface CoordinateInterface {
x: number;
y: number;
}
type CoordinateTuple = [number, number];
type Coordinate = CoordinateInterface | CoordinateTuple;
Intersection Types
interface Fruit {
name: string;
sweetness: number;
}
interface Vegetable {
name: string;
hasSeeds: boolean;
}
type EdibleThing = Fruit & Vegetable;
let apple: EdibleThing = {
name: "Apple",
sweetness: 80,
hasSeeds: true,
}; // This works
Combining primitives will always yield a never
type or a type that is impossible to satisfy.
type Strnumbering = string & number; // type Strnumbering = never;
Literal Types
type Seasons = "spring" | "summer" | "autumn" | "winter";
function seasonMessage(season: Seasons) {
if (season === "summer") {
return "The best season.";
}
return "It's alright, I guess.";
}
seasonMessage("autumn"); // It's alright, I guess.
seasonMessage("fall"); // Type Error: Argument of type '"fall"' is not assignable to parameter of type 'Seasons'.
Classes
Class Definition
class EdibleThing {
name: string;
color: string;
constructor(name: string, color: string) {
this.name = name;
this.color = color;
}
}
class Fruit extends EdibleThing {
sweetness: number;
constructor(name: string, color: string, sweetness: number) {
super(name, color);
this.sweetness = sweetness;
}
}
typeof Fruit; // "function"
const apple = Fruit("Apple", "red", 80); // Type Error: Value of type 'typeof Fruit' is not callable. Did you mean to include 'new'?
const banana = new Fruit("Banana", "yellow", 70); // This works
Class Modifiers
The three class modifiers are public
, private
, and protected
.
readonly
is another modifier which is not limited to classes. You can think of this as the const
variable declaration, but for object properties.
Property Shorthand
TypeScript gives us a shorthand when our constructor parameters are the same as our properties.
class Fruit {
constructor(public name: string, protected sweetness: number) {}
}
const apple = new Fruit("Apple", 80);
console.log(apple); // Fruit { name: "Apple", sweetness: 80 }
Accessors (get and set)
class Fruit {
constructor(protected storedName: string) {}
set name(nameInput: string) {
this.storedName = nameInput;
}
get name() {
return (
this.storedName[0].toUpperCase() + this.storedName.slice(1)
);
}
}
const apple = new Fruit("apple");
apple.name; // "Apple"
apple.name = "banana";
apple.name; // "Banana"
Notice that I store the name on a different property. If I were to try mutating this.name
within my set name()
method, it would call set name()
again, creating an infinite recursive function.
ES Private Fields
class Fruit {
#name: string;
constructor(name: string) {
this.#name = name;
}
get name() {
return this.#name[0].toUpperCase() + this.#name.slice(1);
}
set name(name: string) {
this.#name = name;
}
}
const apple = new Fruit("apple");
console.log(apple.name); // "Apple"
apple.#name = "banana"; // Type Error: Property '#name' is not accessible outside class 'Fruit' because it has a private identifier.
private
or #private
?
ES Private Fields provide “hard privacy”, which means an outside viewer can’t see those properties even if they wanted to.
As a rule of thumb, ES Private fields are generally what you want to use. However, if you can’t target ES Next when you compile your code, or if you can’t afford to have a lot of polyfill code, TypeScript’s private
modifier is a good alternative.
Advanced TypeScript Types
TypeScript Operators
Type Indexed Access
interface Fruit {
name: string;
color: string;
nutrition: { name: string; amount: number }[];
}
type FruitNutritionList = Fruit["nutrition"];
type NutritionFact = Fruit["nutrition"][0];
// Alternatively
type NutritionFact = Fruit["nutrition"][number];
typeof
let rectangle = { width: 100, height: 200 };
type RectangleProperties = keyof typeof rectangle; // type RectangleProperties = "width" | "height"
const
assertions
let rectangle = { width: 100, height: 100 as const };
// let rectangle: {
// width: number;
// height: 100;
// }
let message = "Hello" as const;
// let message: "Hello"
const assortedItems = ["hello", 5, (fruit: Fruit) => {}] as const;
// const assortedItems: readonly ["hello", 5, (fruit: Fruit) => void]
Type Narrowing
Common Type Guards
Primitive types
function sayNameLoud(name: unknown) {
if (typeof name === "string") {
// name is now definitely a string
console.log(`Hey, ${name.toUpperCase()}`);
}
}
Arrays
function combineList(list: unknown): any {
if (Array.isArray(list)) {
// This will filter any items which are not numbers.
const filteredList: number[] = list.filter((item) => {
if (typeof item !== "number") return false;
return true;
});
// This will transform any items into numbers, and turn `NaN`s into 0
const mappedList: number[] = list.map((item) => {
const numberValue = parseFloat(item);
if (isNaN(numberValue)) return 0;
return numberValue;
});
// This does the same thing as the filter, but with a for loop
let loopedList: number[] = [];
for (let item of list) {
if (typeof item === "number") {
loopedList.push(item);
}
}
}
}
Classes
instanceof
operator
class Fruit {
constructor(public name: string) {}
eat() {
console.log(`Mmm. ${this.name}s.`);
}
}
function eatFruit(fruit: unknown) {
if (fruit instanceof Fruit) {
fruit.eat();
}
}
Objects
Using object
instead of unknown
will tell TypeScript to let us attempt to access properties on this value. We can create a Union of the generic object
type and an Interface with the property that we want to access.
interface Person {
name: string;
}
function sayNameLoud(person: object | Person) {
if ("name" in person) {
console.log(`Hey, ${person.name.toUpperCase()}`);
}
}
Handling null
and undefined
Optional Chaining
?.
Optional Chaining only fails if the property is null
or undefined
; falsy values like 0
and false
will pass through correctly.
Nullish Coalescing
??
- only checks if a value is null
or undefined
.
||
- checks against falsy values, not just null
and undefined
.
Non-null Assertion
!.
Assertion Signatures
keyword as
function buttonEventListener(
event: string,
listener: any,
element: HTMLButtonElement,
) {
element.addEventListener(event, listener);
}
const anchor = document.createElement("a");
buttonEventListener(
"click",
() => console.log("Mouse clicked"),
anchor,
);
// Type Error: Argument of type 'HTMLAnchorElement' is not assignable to parameter of type 'HTMLButtonElement'.
buttonEventListener(
"click",
() => console.log("Mouse moved"),
(anchor as HTMLElement) as HTMLButtonElement,
);
// no error
Structural vs Nominal Typing
class Fruit {
isFruit = true;
constructor(public name: string) {}
}
class Apple extends Fruit {
type: "Apple" = "Apple";
constructor() {
super("Apple");
}
}
class Banana extends Fruit {
type: "Banana" = "Banana";
constructor() {
super("Banana");
}
}
If we didn’t have the type
properties, our Apple
and Banana
types would be indistinguishable, since they all would share the same property: name:string
.
All three of these types are equivalent in TypeScript.
class AppleClass {
type: "Apple";
name: string;
}
interface AppleInterface {
type: "Apple";
name: string;
}
type AppleType = {
type: "Apple";
name: string;
};
Other programming languages, like Java and C# use a nominal type system. The word “nominal” refers to the name of the thing, which means if I were to create two classes with an identical structure, but different names, those two classes would be considered different.
We can emulate nominal typing by adding a unique property to our types with a string literal type. This practice is called “branding”, or “tagging”, and is what allowed us to differentiate between the two types in the type system.
We can also create what are called “branded primitives”. This allows us to create primitive values which are only assignable to variables and parameters that match a specific type signature.
type USD = number & { _brand: "USD" };
type EUR = number & { _brand: "EUR" };
let income: USD = 10; // Type Error: Type 'number' is not assignable to type 'USD'.
let VAT = 10 as EUR;
function convertToUSD(input: EUR): USD {
return (input * 1.18) as USD;
}
let VATInUSD = convertToUSD(VAT); // let VATInUSD = USD;
new
APP_NAME=my-app
{
mkdir -p $APP_NAME
cd $APP_NAME
npm init -f
volta pin node
volta pin yarn
yarn add -D typescript ts-node-dev
yarn add -D eslint prettier husky lint-staged eslint-config-prettier
yarn add -D jest ts-jest eslint-plugin-jest
yarn add -D @types/node @types/jest
echo "{ \"singleQuote\": true, \"jsxSingleQuote\": true }" > .prettierrc.json
npx tsc --init
npx eslint --init
yarn ts-jest config:init
rm package-lock.json
}
...
success: pinned [email protected] (with [email protected]) in package.json
success: pinned [email protected] in package.json
...
Created a new tsconfig.json with:
target: es2016
module: commonjs
strict: true
esModuleInterop: true
skipLibCheck: true
forceConsistentCasingInFileNames: true
You can learn more at https://aka.ms/tsconfig.json
You can also run this command directly using 'npm init @eslint/config'.
Need to install the following packages:
@eslint/create-config
Ok to proceed? (y) y
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · node
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb
✔ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-airbnb-base@latest
The config that you've selected requires the following dependencies:
@typescript-eslint/eslint-plugin@latest eslint-config-airbnb-base@latest eslint@^7.32.0 || ^8.2.0 eslint-plugin-import@^2.25.2 @typescript-eslint/parser@latest
✔ Would you like to install them now with npm? · No / Yes
Installing @typescript-eslint/eslint-plugin@latest, eslint-config-airbnb-base@latest, eslint@^7.32.0 || ^8.2.0, eslint-plugin-import@^2.25.2, @typescript-eslint/parser@latest
added 62 packages, removed 212 packages, and audited 265 packages in 10s
85 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
...
git init
- Add
.gitignore
with https://www.toptal.com/developers/gitignore/api/node,yarn,macos,visualstudiocode
vim .gitignore
{
git init
gaa
}
Prettier
ESLint (and other linters)
eslint-config-prettier
- Turns off all rules that are unnecessary or might conflict with Prettier.
- Note that this config only turns rules off, so it only makes sense using it together with some other config.
- Add
"prettier"
to the"extends"
array in your.eslintrc.*
file. Make sure to put it last, so it gets the chance to override other configs.
vim .eslintrc.js
extends: ['airbnb-base', 'prettier'],
Git hooks
Add the following to your package.json
:
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
Husky
Modern native Git hooks made easy 🐶 woof!
Usage
- Edit
package.json > prepare
script
vim package.json
"scripts": {
"dev": "ts-node-dev --respawn src/index.ts",
"test": "jest",
"prepare": "husky install"
},
- Run
prepare
script once
yarn run prepare
$ husky install
husky - Git hooks installed
✨ Done in 0.13s.
- Add hooks:
{
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/pre-commit "yarn run test"
}
husky - created .husky/pre-commit
husky - updated .husky/pre-commit
Jest
Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
eslint-plugin-jest
Add jest
to the plugins
section of your .eslintrc
configuration file. You can omit the eslint-plugin-
prefix:
vim .eslintrc.js
...
plugins: ['@typescript-eslint', 'jest'],
...
Tell ESLint about the environment variables provided by Jest by doing { "env": { "jest/globals": true } }
vim .eslintrc.js
...
env: {
es2021: true,
node: true,
"jest/globals": true,
},
...
Getting Started
mkdir src tests
Let’s get started by writing a test for a hypothetical function that adds two numbers. First, create a sum.ts
file:
vim src/sum.ts
export default function sum(a: number, b: number) {
return a + b;
}
Then, create a file named sum.test.ts
. This will contain our actual test:
vim tests/sum.test.ts
import sum from "../src/sum";
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
ts-jest
A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.
yarn ts-jest config:init
Jest configuration written to "jest.config.js".
yarn test
$ jest
PASS tests/sum.test.ts
✓ adds 1 + 2 to equal 3 (1 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.973 s
Ran all test suites.
✨ Done in 1.64s.
First Commit
{
gaa
git commit -m "setup typescript project"
}