The Typescript Book
Notes on how to use the Typescript language
How to create a frontend development environment for Typescript
from node:alpine
workdir /usr/application
cmd ["npm", "start"]
copy package.json .
run npm install
copy . .
version: '3'
services:
application:
stdin_open: true
build:
context: .
dockerfile: Dockerfile.dev
ports:
- 3010:3000
volumes:
- .:/usr/application
- /usr/application/node_modules
(1) create a directory called "frontend"
$ mkdir frontend && cd frontend
(2) create the react application in a directory
$ npx create-react-app . --typescript
(3) delete node_modules that create-react-app created in your local directory i.e.
$ rm -rf node_modules
(4) build an image based on your dockerfile.dev
$ docker-compose build
(5) start up a container based on your built image
$ docker-compose up
(6) go to http://localhost:3010 to see the deployed application
(7) make changes in your local directory and see them reflected in the browser
(8) shut down your container when you are done
$ docker-compose down
// To build your docker image
$ docker-compose build
// To start up your docker container
$ docker-compose up --detach
// To get shell access to your running docker container
$ docker-compose exec application sh
// To get information about your running docker container
$ docker-compose ps
// To shut down your docker container
$ docker-compose down
// To install vim in your container
$ docker-compose exec server sh
# apk add vim
How to create a backend development environment for Typescript
FROM node:alpine
WORKDIR /usr/server
CMD ["npm", "run", "development"]
COPY package.json .
RUN npm install
COPY . .
version: '3'
services:
server:
stdin_open: true
build:
context: .
dockerfile: dockerfile.dev
ports:
- 4010:4000
volumes:
- .:/usr/server
- /usr/server/node_modules
import express from 'express'
const server: express.Application = express()
server.get('/', (request, response) => {
response.send('bye')
})
server.listen(4000, () => console.log('server running'))
{
"name": "backend",
"version": "1.0.0",
"description": "backend playground",
"main": "build/server.js",
"scripts": {
"tsc": "tsc",
"development": "ts-node-dev --poll --respawn --transpileOnly ./source/server.ts",
"production": "tsc && node ./build/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "4.17.1"
},
"devDependencies": {
"@types/express": "4.17.6",
"ts-node-dev": "1.0.0-pre.44",
"typescript": "3.9.3"
}
}
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
(1) create a directory called backend
$ mkdir backend && cd backend
(2) copy over the required files into the backend directory i.e.
dockefile.dev, docker-compose.yml, source/server.ts, package.json, tsconfig.json
(3) build the docker image
$ docker-compose build
(4) build the docker container
$ docker-compose up --detach
(5) go to http://localhost:4010 to see the deployed server
(6) make changes in your local directory then reload browser to see changes
(7) shut down your docker container when you are done
$ docker-compose down
// To build your docker image
$ docker-compose build
// To start up your docker container
$ docker-compose up --detach
// To get shell access to your running docker container
$ docker-compose exec server sh
// To get information about your running docker container
$ docker-compose ps
// To shut down your docker container
$ docker-compose down
// To install vim in your container
$ docker-compose exec server sh
# apk add vim
Three cases where you need explicit type annotation
As a rule you should rely on type inference. In 3 specific cases you need to use explicit type annotation.
// When function that returns the "any" type e.g. JSON.parse()
// interface Point {
// x: number
// y: number
// }
// const thePoint: Point = { x: 10, y: 20 }
const thePoint = { x: 10, y: 20 }
const coordinates = JSON.parse(JSON.stringify(thePoint))
// Solution#1 const coordinates = JSON.parse(JSON.stringify(thePoint)) as Point
// Solution#2 const coordinates: Point = JSON.parse(JSON.stringify(thePoint))
console.log(coordinates)
// When variable is declared and assigned rather than initialised
let words = ['red', 'green', 'blue']
let found
// Solution#1 let found: boolean
// Solution#2 let found = false
found = Boolean(words.find((word) => word === 'green'))
console.log(found)
// When variable type cannot be inferred correctly
let numbers = [-10, -1, 12]
let numberAboveZero
// Solution#1 let numberAboveZero: number | boolean = false
numberAboveZero = numbers.find((number) => number > 0) || false
console.log(numberAboveZero)
How to annotate a function
// Case 1: Annotate variable
const sum
: (a: number, b: number) => number
= (a, b) => a + b
// Case 2: Annotate function
const difference
= (a: number, b: number): number
=> a - b
How to annotate an object
const person
: { name: string, age: number,
coordinates: { longitude: number, latitude: number }}
= { name: 'bob', age: 35,
coordinates: { longitude: 1.23456, latitude: 6.54321 }}
interface Person {
name: string,
age: number,
coordinates: { longitude: number, latitude: number }
}
const person
: Person
= { name: 'bob', age: 35,
coordinates: { longitude: 1.23456, latitude: 6.54321 }}
How to annotate a destructured value
const todaysWeather = {
date: new Date(),
weather: 'sunny',
}
// Case 1: parameter is not destructured
const logWeather1 = (forecast: { date: Date; weather: string }): void => {
console.log(forecast.date)
console.log(forecast.weather)
}
logWeather1(todaysWeather)
const todaysWeather = {
date: new Date(),
weather: 'sunny',
}
// Case 2: parameter is destructured inside function body
const logWeather2 = (forecast: { date: Date; weather: string }): void => {
const { date, weather } = forecast
console.log(date)
console.log(weather)
}
logWeather2(todaysWeather)
const todaysWeather = {
date: new Date(),
weather: 'sunny',
}
// Case 3: parameter is destructured inside function header
const logWeather3 = ({
date,
weather,
}: {
date: Date
weather: string
}): void => {
console.log(date)
console.log(weather)
}
logWeather3(todaysWeather)
How to annotate arrays
// homogenous arrays
const array: string[] = []
array.push('')
const value = array.pop() // value inferred to be of type string
// heterogenous arrays
const array: (string | number)[] = []
array.push('')
array.push(0)
const value = array.pop() // value inferred to be of type string or number
How to annotate tuples
// A tuple is an array that expects specific types at specific indexes
// The code below shows how to use an enumeration to enforce this ordering
// define enumeration to provide access to tuple elements at specific indicies
enum Quality { color, fizzy, calories }
const { color, fizzy, calories } = Quality
// define the type of the tuple
type Drink = [string, boolean, number]
// define an instance of a tuple
const pepsi: Drink = ['brown', true, 40]
pepsi[color] = 40 // error - cannot assign number to string
pepsi[fizzy] = 'brown' // error - cannot assign string to boolean
pepsi[calories] = true // error - cannot assign boolean to number
How to create types in Typescript
// (1) create a type for a function
type predicate = (value: number) => boolean
const isEven: predicate = (value) => value % 2 === 0
// (2) create a type for an object
type Person = { name: string; age: number }
const robert: Person = { name: 'bob', age: 35 }
// (3) create a type for a tuple
type Beverage = [string, boolean, number]
const drPepper: Beverage = ['brown', true, 75]
// (4) create a stype for a heterogenous array
type Dates = (string | Date)[]
const rolodex: Dates = ['2020-09-25 10:00:05', new Date()]
How to create interfaces in Typescript
interface Reportable {
name: string
year: number
broken: boolean
print(): void
}
const car = {
name: 'civic',
year: 2000,
broken: true,
print() {
console.log(`Name: ${this.name}`)
console.log(`Year: ${this.year}`)
console.log(`Broken: ${this.broken}`)
},
}
const printVehicle = (vehicle: Vehicle) => {
vehicle.print()
}
printVehicle(car)
How to add method and property accessibility specifiers
class Vehicle {
// only callable within class and subclasses
protected drive() {
console.log('chugga-chugga!')
}
// callable by anyone on class instance or subclass instance
public honk() {
console.log('beep-beep!')
}
// callable only within class
private price() {
console.log('$1200')
}
}
class Car extends Vehicle {
// visibilty of protected method can be increased when overriding
public drive() {
console.log('vroom-vroom!')
}
}
const car = new Car()
console.log(car.drive())
console.log(car.honk())
class Vehicle {
constructor(
// only usable within class instance
private color: string,
// usable on any class instance or subclass instance
public cost: number,
// usable only withinany class instance or subclass instance
protected engine: string
) {}
public getColor() {
return this.color
}
toString() {
return `color: ${this.color} cost: ${this.cost} engine: ${this.engine}`
}
}
class Car extends Vehicle {
// Car constructor implicitly chained to Vehicle constructor for us!
}
class BlueFastCar extends Vehicle {
// constructor calls super class constructor explicitly
constructor(public wheels: number) {
super('blue', 1400, 'v12')
}
// overridden toString calls superclass toString for help
public toString() {
return `${super.toString()} wheels: ${this.wheels}`
}
}
const vehicle1: Vehicle = new Car('red', 1200, 'v8')
console.log(vehicle1.getColor())
console.log(vehicle1.cost)
console.log(vehicle1.toString())
const vehicle2: Vehicle = new BlueFastCar(4)
console.log(vehicle2.getColor())
console.log(vehicle2.cost)
console.log(vehicle2.toString())
How to use generics with constraints
class Printer<T> {
constructor(private valueOne: T) {}
print(valueTwo: T): T {
console.log(valueTwo)
return this.valueOne
}
}
const printer = new Printer<number>(42)
printer.print(24)
// printer.print('invalid value')
function echo<T>(value: T): T {
console.log(value)
return value
}
echo<number>(1)
// echo<number>('invalid value')
interface Printable {
print(): void
}
class Car implements Printable {
print(): void { console.log('Car') }
}
class Van implements Printable {
print(): void { console.log('Van') }
}
function list<T extends Printable>(array: T[]) {
array.forEach((item) => item.print())
}
list([new Car(), new Van()])
How to use decorators
class Boat {
@log('I', 'could')
pilot(): void {
throw new Error('not pilot boat')
}
}
function log(a: string, b: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const method = descriptor.value
descriptor.value = function () {
try {
method()
} catch (error) {
console.log(`${a} ${b} ${error.message}`)
}
}
}
}
const boat = new Boat()
boat.pilot() // prints the message "I could not pilot boat"
// ToDo...
// ToDo...
// ToDo...
How to use React prop types with Typescript
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// (1) These are where you define the prop types for your component
interface AppProps {
// (2) Using JSDOC comments show up in Visual Code on hover 🥰
/** The text to show */
text?: string
}
class App extends Component<AppProps> {
// (3) These are your default prop values
static defaultProps = {
text: 'Not Available',
}
render() {
return <h1>{this.props.text}</h1>
}
}
ReactDOM.render(<App />, document.getElementById('root'))
import React, { Component, Fragment } from 'react'
import ReactDOM from 'react-dom'
interface CounterProps {
text: string
count: number
increment(event: React.MouseEvent<HTMLElement>): void
decrement(event: React.MouseEvent<HTMLElement>): void
}
const Counter = ({ text, count, increment, decrement }: CounterProps) => (
<Fragment>
<h1>
{text} {count}
</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</Fragment>
)
interface AppProps {
text: string
}
interface AppState {
count: number
}
class App extends Component<AppProps, AppState> {
constructor(props: AppProps) {
super(props)
this.state = { count: 0 }
}
increment = (): void =>
this.setState((oldState) => ({ ...oldState, count: oldState.count + 1 }))
decrement = (): void =>
this.setState((oldState) => ({ ...oldState, count: oldState.count - 1 }))
render() {
const { text } = this.props
const { count } = this.state
const { increment, decrement } = this
return (
<Counter
text={text}
count={count}
increment={increment}
decrement={decrement}
/>
)
}
}
const app = <App text="The count is" />
const node = document.getElementById('root')
ReactDOM.render(app, node)
How to use useRef instead of useState to represent a form
The usual approach for a form is to use state and set up change handlers on each input field. However this results on change handlers being fired on every keypress, potentially a large number of events. Since we are only interested in the contents of the input fields on a form submission, we can use refs to the input fields and only grab their values once when there is a form submission.
import {
FormEvent,
FormEventHandler,
FunctionComponent,
useRef,
RefObject,
} from 'react'
const getAndReset = (ref: RefObject<HTMLInputElement>): string => {
if (!ref.current) return ''
const value = ref.current.value
ref.current.value = ''
return value
}
export const SignIn: FunctionComponent = (): JSX.Element => {
const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const handleSubmit: FormEventHandler = (
event: FormEvent<HTMLFormElement>
): void => {
event.preventDefault()
const email = getAndReset(emailRef)
const password = getAndReset(passwordRef)
console.log('form submission', email, password)
}
return (
<div className="sign-in">
<h2>I already have an account</h2>
<span>Sign in with your email and password</span>
<form onSubmit={handleSubmit}>
<input name="email" type="email" required ref={emailRef} />
<label htmlFor="email">Email</label>
<input type="password" name="password" ref={passwordRef} />
<label htmlFor="password">Password</label>
<input name="submit" type="submit" value="Submit Form" />
</form>
</div>
)
}
Last updated