A Gentle Introduction to TypeScript
Talk given at Fullstack Talks (NC San Diego) - 02/2o/2020
This information has been updated since the talk (mostly extra info added, some grammar/spelling errors corrected).
Who am I?
- Principal Software Engineer at Peachjar
- 3/4 Stack Developer (or Pear-shaped Developer)
- Former Marine Corps Meteorologist
- Virgo
- Foodie
- Reluctant TypeScript Zealot
Peachjar's History w/ TypeScript
- Hired at Peachjar, told it would be our primary language for our new stack.
- No one had professional experience with TypeScript
- I started learning TypeScript 2 weeks before I joined PJ.
- We went from 0 lines of TS to 4 React/Apps, ~30 microservices, and 3 internal service frameworks (it will be 2 years in April).
- We are now starting to codify our CI pipelines in TS (using Github Actions).
Mike made feel insecure about my level of TS knowledge, so I show you this to prove how much we use TS. 😞😞😞
Roadmap
- Compilation and Running TypeScript
- Basic TypeScript
- Intermediate TypeScript
- Why TypeScript: Myths, Advantages, and Disadvantages of TypeScript
- Code from Live Demo
Compilation and Running TypeScript
# Install globally or into a project
npm i -g typescript
# Create a file with the ".ts" extension
echo 'console.log("Hello");' > index.ts
# Compile using the "tsc" command
tsc index.ts
# The file will be compiled in the same dir
ls
> index.js index.ts
# Run the "compiled" file
node index.js
# You can also "skip" the compilation step and directly
# with ts-node (but this is slow)
npm i -g ts-node
ts-node index.ts
There are a ton of ways to configure the TS build process (like generate source maps, push code to a build directory, etc.). Most teams will develop conventions around how they build and run TS projects.
NOTE: during the blooper reel that was my live demo, you saw that the standard configuration of TS does not include the ES2017 features. You will need a tsconfig.json
file to enable that feature.
Basic TypeScript
- Primitives
- Arrays
- Tuples
- Objects
- Type Aliases
- Enums
-
null
,undefined
,any
- Functions
Primitives:
// When you initialize a variable without a type,
// the compiler will give it the most restrictive type
// based on the value.
const foo = 'bar'
const foo: string = 'bar'
const bar = 1
const bar: number = 1
const yomama = true
const yomama: boolean = true
let isReassignable = true
isReassignable = false
isReassignable = 'no' // compile error (type is boolean)
Arrays (homogenous - items of the same type)
const sodas = ['pepsi', 'coke']
const sodas: string[] = ['pepsi', 'coke']
// More complex:
const bucket = [1, 'hello', true]
// Any element in the array can be a number, string, or boolean
const bucket = (number | 'string' | boolean)[] = [1, 'hello', true]
Tuples (heterogeneous - items of specific types)
If not familiar, it's a "finite ordered list (sequence) of elements" (Wikipedia).
const latlon: [number, number] = [33.123456, -116.134123]
const wind: [string, number, number] = ['SW', 10, 15]
Objects
const options = {
username: 'foo',
password: 'bar',
timeout: 1000,
}
const options: { username: string; password: string; timeout: number } = {
username: 'foo',
password: 'bar',
timeout: 1000,
}
^ this is ugly, right?
Type Aliases
type BucketItem = number | 'string' | boolean
type Bucket = BucketItem[]
const bucket: Bucket = [1, 'hello', true]
type LatLon = [number, number]
const latlon: LatLon = [33.123456, -116.134123]
type Options = {
username: string
password: string
timeout: number
}
const options: Options = {
username: 'foo',
password: 'bar',
timeout: 1000,
}
Type Aliases of Primitives lead to better clarity
type int = number
type milliseconds = int
type Options = {
username: string
password: string
timeout: milliseconds
}
But we can even do better with type restrictions
type knots = int
type Direction = 'N' | 'NE' | 'E' | 'SE' | 'S' | 'SW' | 'W' | 'NW'
type SustainedSpeed = knots
type GustSpeed = knots
type WindMeasurement = [Direction, SustainedSpeed, GustSpeed]
const wind: WindMeasurement = ['SW', 10, 15]
Unfortunately, types are inaccessible at runtime
console.log(Direction) // compile error
// This is true of any type
const gust: GustSpeed = 25
const isGust = typeof gust === 'GustSpeed' // false
A better strategy for "enumerated" values is an enum
enum Direction {
North = 'N',
NorthEast = 'NE',
East = 'E',
SouthEast = 'SE',
South = 'S',
SouthWest = 'SW',
West = 'W',
NorthWest = 'NW',
}
// Under the hood, enums are just objects:
console.log(Object.keys(Direction))
// I use this aspect with validation:
import * as Joi from 'joi'
const DirectionSchema = Joi.string()
.valid(Object.values(Direction))
.required()
const { error } = Joi.validate('SW', DirectionSchema)
null
let foo = null
foo = 'bar' // compile error
// If we want something to be null or something else, we have to declare
// the type that way:
let foo = string | null = null
foo = 'bar'
undefined
type Options = {
foo: string | undefined
bar: number
}
const options: Options = {
bar: 1234,
}
const options: Options = {
foo: 'baz',
bar: 1234,
}
TypeScript will enforce checks of null
and undefined
properties before use
const options: Options = {
bar: 1234,
}
// compile error (null | string) not compatible with type "string"
const foo: string = options.foo
// But you can tell the compile you know better:
const foo: string = options.foo!
// ^ but here's the gotcha - YOU DIDN'T KNOW BETTER!
// This works because we checked for the existence of "foo"
const foo = options.foo ? options.foo : 'unknown'
"Optional Chain" Operator
type Link = {
id: number,
to: Link | undefined | null,
from: Link | undefined | null,
}
const head: Link = { id: 1 }
const link2: Link = { id: 2, from: head }
head.to = link2
const link3: Link = { id: 3, from: link2 }
link2.to = link3
const link4: Link = { id: 4, from: link3 }
link3.to = link4
const fourDeep = head.to?.to?.to // Should be link 4
// undefined
const wayBack = link4.from?.from?.from?.from?.from?.from?.from?.from
any
(try not to use any
)
// the value can be anything (welcome back to JS)
let foo: any
foo = {}
foo = 'yeah'
foo = 123
We will come back to "any" in a little bit
functions
function addTwo(num: number): number {
return num + 2
}
const addTwo = (num: number): number => num + 2
// Since functions can be passed, they also can have type definitions:
const addTwo: (num: number) => number = function addTwo(num: number): number {
return num + 2
}
const addTwo: (num: number) => number = (num: number): number => num + 2
// In general, I recommend using type definitions for clarity:
type AddTwo = (num: number) => number
const addTwo: AddTwo = (num: number): number => num + 2
void
(indicate no return value)
function printHello(name: string): void {
console.log(`Hello: ${name}`)
}
function printHello(name: string): void {
console.log(`Hello: ${name}`)
return
}
In some cases any
is great for succinct functions (but bad for indicating intent)
function purchaseCart(cart: Cart): any {
if (cart.items.length === 0) {
return console.log('No items in cart!')
}
// return nothing
sendPurchaseToCheckout(cart)
}
never
(rarely used, but you should be aware of it)
function sendPurchaseToCheckout(cart: Cart): never {
throw new NotImplementedError()
}
variadic arguments
function printGreeting(greeting: string, ...names: string[]) {
for (const name of names) {
console.log(`${greeting} ${name}$`)
}
}
printGreeting('Hello', 'Richard', 'Brandee', 'Aurora', 'Sophia')
Intermediate TypeScript
- Classes
- Interfaces
- Generics
Classes (yey, OOP!)
class Foo {
yo: string
mama: number
constructor(yo: string, mama: number) {
this.yo = yo
this.mama = mama
}
get yoMama(): string {
return `yo: ${this.yo}, mama: ${this.mama}`
}
}
Inheritance works just like you would think:
class Bar extends Foo {
constructor() {
super(yo, mama)
}
printMama(): void {
console.log(this.mama)
}
}
Simplified visibility and property setting
class Foo {
constructor(public yo: string, public mama: number) {}
}
class Foo {
constructor(protected yo: string, private mama: number) {}
protected bar() {}
// methods can be set like any other property in JS.
private alwaysTrue = () => true
}
Interfaces
interface Notifier {
myField: string
notify(userId: string, message: string): void
}
class ConsoleNotifier implements Notifier {
myField = 'foo'
notify(userId: string, message: string): void {
console.log(`Hey ${userId}, ${message}`)
}
}
Function Interfaces
interface notify {
(userId: string, message: string): void
}
const consoleNotify: notify = (userId: string, message: string): void =>
console.log(`Hey ${userId}, ${message}`)
// One thing I have not mentioned yet is that a lot of times
// you can leave off the param/return types if TS is told
// what type a "thing" is supposed to be.
const consoleNotify: notify = (userId, message) =>
console.log(`Hey ${userId}, ${message}`)
Duck Typing
interface notify {
(userId: string, message: string): void
}
function doThingAndNotify(notify: notify): void {
// do'in it
notify()
}
// console.log is able to meet the type requirement, therefore
// the compiler "considers" it to have "implemented" the interface
doThingAndNotify(console.log)
Types vs Interfaces
There are not many practical differences between interfaces and types.
- Interfaces can be extends
- Interfaces can be explicitly 'implemented'
- Types cannot be redeclared in the same context; if interfaces are redeclared, their properties are merged.
type Foo = string
type Foo = string // compile error
interface Bar {
prop1: string
}
interface Bar {
prop2: string
}
Advanced TypeScript (sorry, not today)
- Symbols
- Advanced Generics
- Modules
- Namespaces
- Decorators
Why TypeScript?
Myths about TS:
- Types will increase the complexity of my codebase (you already have a ton of hidden complexity that you don't know about.)
- Types will force me to use OOP (you can be 100% functional, and I would argue you will get closer to categorical compositions because of types [e.g. monads, etc]. I will say, if you are familiar with Java and .NET, TS will be easier).
- I won't be able to use pure JavaScript libraries (you can create type definitions for external libs)
- Err me gerd, not another CoffeeScript/Flow/whatever (fair - but I think TS has far greater traction).
- TS code will be unusable in pure JavaScript (TS is just JS).
- TS is going to dramatically slow development (I think its the opposite, really).
- Unit tests are enough - I don't need TS! (oh boy, wait until you see the holes in your code.)
- TS is all I need - No more unit tests! (oh boy, wait until you see the holes in your code.)
Benefits of TypeScript
- Greater clarity in what your code ACTUALLY does.
- Ability to model abstract concepts divorced from implementations (which JS does not support).
- Intuitive type system (it doesn't feel like writing Java code in JavaScript).
- Compile time checks to catch obvious errors.
- All the great futuristic JS features (now!)
- Fantastic ecosystem (actually, I would say TS type definitions are the de facto standard for new libs).
- Awesome IDE support (better than JavaScript).
- A reduction in the need for JSDocs (code documentation) -- you should strive for greater code clarity (make it a game - how can my code be so obvious that the type system alone is all I need to describe the functionality).
Drawbacks of TypeScript
- No runtime introspection of types (the type system doesn't exist after compilation).
- Complex types (with generics) can sometimes be confusing
- Having to create type definitions for plain JS libraries can be a sad process.
Demo Code:
Setup
npm init
npm i -S typescript axios
mkdir src
touch src/index.ts
cp ~/.../tsconfig.json .
tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["dom", "es2017", "esnext.asynciterable" ],
"declaration": true,
"sourceMap": true,
"outDir": "./dist/",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
Add Build Step to package.json
{
// ...
"scripts": {
"build": "rm -rf dist && ./node_modules/.bin/tsc --pretty",
},
// ...
}
src/index.ts
import Axios from 'axios'
const hollaApiUrl = "<holla-api>"
async function main(): Promise<void> {
const { data } = await Axios.get(`${hollaApiUrl}/say/hi-fullstackers`)
console.log(data)
}
main()
Compile and Run
npm run build
node dist/index.js
Output
Not pretty with the HTML entities, but it works! Mark actually created a version that rendered beautifully on the command line, but I don't have the URL to the new instance of the service in ECS.
Conclusion
- Demonstrated Basic and Intermediate Usage of TS
- Discussed Strengths and Weakness of TS
Stumbling my way through the great wastelands of enterprise software development.