Jack Herrington YouTube
typescriptlang.org
Why to use?
1.) Reduce Errors. Examples include: try to call something off of a null or undefined variable. Call a function without the right parameters. Don’t know the fields, or you put the wrong fields.
2.) Code Faster: Be able to know right away what the fields are, and if you’re missing fields.
npm init -y
// then
npm run typescript ts-node
// then
npx tsc --init
Runs the files with the following command:
npx ts-node filename.ts
TS infers types and the golden hotkey to see what it is inferring is “ctrl k ctrl i”
let userName: string = "Brandon"
let hasLoggedIn: boolean = true
let myNumber: number = 10
let myRegex: RegExp = /foo/
let myArr: string[] = ["one", "two", "three"]
Another way to do arrays is with the generic type
const myValues: Array<number> = [1, 2, 3]
Objects
const myPerson: {
first: string;
last: string;
} = {
first: "Tom",
last: "Ace"
}
But you don’t want to copy and paste this all over the place so instead you will use an interface
interface Person {
first: string;
last: string;
}
const myPerson: Person = {
first: "Tom",
last: "Ace"
}
We oftentimes use objects as maps so here’s how we do that using a utility type called “Record”:
const ids: Record<number, string> = {
10: "a",
20: "b"
}
ids[30] = "c"
TS with conditionals is the same.
if (ids[30] === "D") {
// the condition is false because it wants a number
}
And loops are the same, although if TS is inferring correctly then you don’t wan to set the type as you want to let TS infer as much as possible.
for (let i: number = 0; i < 10; i++) {
console.log(i)
}
Exporting and importing functions from different files, TS does not like the “module.exports = functionName” syntax so instead use “export default functionName” and import with ‘const blank = require(“filename”)’ or use a named export.
What is the “any” type? It’s the type we are trying to avoid, it can be anything, (a number, a string, an array, an object, undefined, null, anything). We want to control the type and specify what we have.
Typing parameters and returns
function addNumber(a: number, b:number): number {
return a + b
}
Const functions (import with brackets to destructure)
export const addStrings = (str1: string, str2: string): string => {
`${str1} ${str2}`
}
Default parameters
export const addStrings = (str1: string, str2: string = ""): string => {
`${str1} ${str2}`
}
Union Types
export const format = (title: string, param: string | number): string => {
`${title} ${param}`
}
Void functions. When you are not returning anything at all, (remember “console.log” is considered a side-effect).
export const printFormat = (title: string, param: string | number): void => {
// format function defined earlier
console.log(format(title, param))
}
Promise functions
export const fetchData = (url: string): Promise<string> => {
Promise.resolve(`Data from ${url}`)
}
Note: if you get a warning that the promise is not defined go into you “tsconfig.json” file and change the “target” to “esnext.”
Rest parameters, (multiple arguments and then coalesce into an array)
function introduce(salutation: string, ...names: string[]): string {
return `${salutation} ${names.join(' ')}`
}
Biggest misconception is when types are enforced. Types are not enforced on runtime but on compilation time!
compile a ts file into a js file with the following command:
npx tsc filename.ts
// outputs a filename.js file
node filename.js
So if you are exporting some functions in a .ts module file and compile them down to a “.js” file, and import and use in a different “.js” file, you can still get errors if you call a function that is expecting arguments with no arguments so implement type safety not just with types but with some handy helper stuff like the optional chaining strategy. As an example back in the TS code,
export function getName(user: {first: string, last: string}): string {
return `${user?.first} ${user?.last}`
}
// makes sure user is defined before we reference it
This will add some checking into the compiled version of the function to make sure the user actually exists before we can reference it.
export function getName(user: {first: string, last: string}): string {
return `${user?.first ?? "first"} ${user?.last ?? "last"}`
}
// the nullish coalescing operator adds some defaults
// to prevent getting undefined undefined
#3 – Typescript Functions with Functions
Functions that take functions or create functions
function parameters
export function printToFile(text: string, callback: () => void): void {
console.log(text)
callback()
}
function with params
export function arrayMutate(numbers: number[], mutate: (v:number) => number): number []{
return numbers.map(mutate)
}
console.log(arrayMutate([1, 2, 3], (v) => v.map(v * 10)))
// expected: [10, 20, 30]
If the above seems hard to read can convert to a function as a type, (this could be exported itself to be reused).
type mutationFunction = (v: number) => number
export function arrayMutate(numbers: number[], mutate: muationFunction): number[] {
return numbers.map(mutate)
}
console.log(arrayMutate([1, 2, 3], (v) => v.map(v * 10)))
// expected: [10, 20, 30]
Returning functions
An example of a classic JS closure:
export function createAdder(num: number) {
return (val: number) => num + v
}
const addOne = createAdder(1)
console.log(addOne(10))
// expected: 11 because the 1 was captured by the closure
So to define the output type in this:
export function createAdder(num: number): (val: number) => number {
return (val: number) => num + v
}
const addOne = createAdder(1)
console.log(addOne(10))
// expected: 11 because the 1 was captured by the closure
But same as before, if this reads funky you can define as a function type and reference that:
type AdderFunc = (val: number) => number
export function createAdder(num: number): AdderFunc {
return (val: number) => num + v
}
const addOne = createAdder(1)
console.log(addOne(10))
// expected: 11 because the 1 was captured by the closure
It really is worth getting familiar with functions that take in functions and function s that return functions because there is a lot of hype around functional programming and a lot of the frameworks use this stuff too.
#4 – Function Overloading in Typescript
Often overlooked feature of TS.
Want to create a function that parses coordinates but we want them to be able to be specified in a number of different ways.
interface Coordinate {
x: number,
y: number
}
Implementation #1
function parseCoordinateFromObject(obj: Coordinate): Coordinate {
return {
...obj,
}
}
function parseCoordinateFromNumbers(x: number, y: number): Coordinate {
return {
x,
y,
}
}
This works just fine but it doesn’t feel like JavaScript. Normally we would just have a “parseCoordinate” function that will look at what is coming in and depending on that, return the same. So the TS feature we are looking for to do this is called function overloading.
Implementation #2
Define the variants you want immediately above. Then define a function with parameters having an “unknown” type. What is an “unknown” type? It is basically an “any” but you have to cast it before you use it; kind of like a safe “any.”
Then you have to tell TS which parameters are optional and we do that with a question mark, (?). Then we use some typecasting with “typeof” and the TS keyword “as.”
function parseCoordinate(obj: Coordinate): Coordinate;
function parseCoordinate(x: number, y: number): Coordinate;
function parseCoordinates(arg1: unknown, arg2?: unknown): Coordinate {
let coord: Coordinate {
x: 0,
y: 0,
}
if (typeof arg1 === "object") {
coord = {
...(arg1 as Coordinate)
}
} else {
coord = {
x: arg1 as number,
y: arg2 as number
}
}
return coord;
}
console.log(parseCoordinate(10, 20))
console.log(parseCoordinate(x: 52, y: 35))
When we call the function above, we can see in the VSCode hinting, it says “1/2” meaning there are two signature you can choose from.
How can you handle a string variant, (“x:12,y:22”)? Define another variant above the exisiting ones and add another “else if” in the typecasting. Will need to run split method on the “,” and then a forEach that runs a split on the “:” and you will also need the “as” keyword for the key as well:
if (typeof arg1 === "string") {
(arg1 as string).split(",").forEach(str => {
const [key, value] = str.split(":")
coord[key as "x" | "y"] = parseInt(value, 10)
})
}
#5 – Optionals in Typescript
Optional parameters
function printIngredient(quantity: string, ingredient: string, extra?: string) {
console.log(`${quantity} ${ingredient}`)
}
Optional parameters need to come at the end of the parameter list.
How can you check for an optional parameter’s existence?
// what a crazy example? nested backticks
console.log(${quantity} ${ingredient} ${extra ?
`${extra}` : ""}`)
optional fields (like in an object):
interface User {
id: string,
info?: {
email?: string
}
}
// old school
// using ! means you know better than TS, it will be there
// if you're using ! a lot, you're probably not doing the right thing
function getEmail(user: User): string {
if (user.info) {
// return user.info.email!
}
return ""
}
// new school easy way
// notice the optional chaining feature
// also notice the nullish coalescing operator
function getEmailEasy(user: User): string {
return user?.info?.email ?? ""
}
Optional callbacks
function addWithCallback(x:number, y:number, callback: () => void) {
console.log([x, y])
callback()
}
Callback could be optional by using question mark and then wrapping in conditional
function addWithCallback(x:number,y:number, callback?: () => void) {
console.log([x, y])
if (callback) {
callback()
}
}
or by using question mark (when defining parameter) and then using “?.” syntax
function addWithCallback(x:number,y:number, callback?: () => void) {
console.log([x, y])
callback?.()
}
#6 – Tuples in Typescript
type ThreeDCoordinate = [x: number, y: number, z: number]
function add3DCoordinate(c1: ThreeDCoordinate, c2: ThreeDCoordinate): ThreeDCoordinate {
return [
c1[0] + c2[0],
c1[1] + c2[1],
c1[2] + c2[2]
]
}
Another example: React devs comes across the “useState” tuple all the time.
function simpleStringState(initial: string): [() => string, (v:string) => void] {
let str: string = initial
return [
() => str,
(v: string) => {
str = v
}
]
}
// note this function is a closure
const [str1getter, str1setter] = simpleStringState("hello")
#7 – Generics in Typescript
Continuing with example below what would happen if we replaced string with a different type, any type, (not the “any” type but a type that we specify).
function simpleState<T>(initial: T): [() => T, (v: T) => void] {
let val: T = initial
return [
() => val,
(v: T) => {
val = v
}
]
}
// note this function is a closure
const [state1getter, state1setter] = simpleState(10)
Once we call the function with a number, every place where T is will become a number.
There is a small issue here when calling the function with null as initial state because we need to override T when we call it.
const [state1getter, state1setter] = simpleState<string | null>(null)
Another example with a function called “ranker.”
function ranker<RankItem>(items: RankItem[], rank: (v: RankItem) => number): RankItem[] {
interface Rank {
item: RankItem;
rank: number;
}
const ranks: Rank[] = items.map(item => {
item,
rank: rank(item)
})
ranks.sort((a, b) => a.rank - b.rank)
return ranks.map(rank => rank.item)
}
But interface Rank only has access to RankItem since it is defined inside the function. So here is what we can do instead,
interface Rank<RankItem> {
item: RankItem;
rank: number;
}
function ranker<RankItem>(items: RankItem[], rank: (v: RankItem) => number): RankItem[] {
const ranks: Rank<RankItem>[] = items.map(item => {
item,
rank: rank(item)
})
ranks.sort((a, b) => a.rank - b.rank)
return ranks.map(rank => rank.item)
}
A cool thing about TS is that you can make almost anything a generic, (class, function, interface, a type). Super powerful.
interface Pokemon {
name: sting;
hp: number;
}
const pokemon: Pokemon[] = [
{
name: "Bulb",
hp: 20,
},
{
name: "Pikachu",
hp: 50,
}
]
const ranks = ranker(pokemon, ({hp}) => hp)
console.log(ranks)
// generic ranking algorithm
#8 – Generics with keyof in Typescript
pluck example
function pluck<DataType, KeyType extends keyof DataType>(
items: DataType[],
key: KeyType
): DataType[KeyType][] {
return items.map(item => item[key])
}
const dogs = [
{name: "Mimi", age: 12},
{name: "LG", age: 13}
]
console.log(pluck(dogs, ""))
Note: the reason this is cool is when you call the pluck function, when you put quote in the second argument you get hinting at what is available to you.
Example #2 Event Map
interface BaseEvent {
time: number;
user: string;
}
interface EventMap {
addToCart: BaseEvent & {quantity: number; productID: string},
checkout: BaseEvent
}
function sendEvent<Name extends keyof EventMap>(name: Name, data: EventMap[name]): void {
console.log([name, data])
}
sendEvent("addToCart", {productID: 'foo', and more...})
Note the single ampersand meaning take this type and add it to this type.
The important thing here is that you cannot mess up.
#9 – Typescript Utility Types
Mechanism you can use to create another type from an existing type in interesting ways. “Partial” and “Record” are popular versions of these.
“Partial” easily makes a defined interfaces field’s optional
type MyUserOptionals = Partial<MyUser>
“Required” takes off optionality in a pre-defined interface
“Pick” is another utility type shown.
“Record” example:
const mapById = (users: NyUser[]): Record<string, MyUser> => {
return users.reduce((a, b) => {
return {
...a,
[v.id]: v,
}
}, {})
}
console.log(mapById([
{id: "green", name: "Mr. Green"},
{id: "plum", name: "Professor Plum"}
]))
“Omit” does the opposite of “Pick”
#10 – Readonly And Immutability in Typescript
Not good you can change a cat’s name
add “readonly” in front of a field’s name
One way is to make a readonly cat:
function makeCat(name: string, breed: string): Readonly<Cat> {//...}
“readonly” tuples
the issue with “const”
const reallyConst = [1, 2, 3] as const
#11 – Enums and Literal Types in Typescript
enum LoadingState {
beforeLoad = "beforeLoad",
loading = ""loading,
loaded = "loaded",
}
Note: don’t need to do the equality with the string but it may be a good idea to leave it for console.log purposes.
Literal types
roll dice example
constrain the number down to values of dice
Numeric Literals
String Literals
#12 – Typescript Classes; Member Visibility and Implements
Create a simple NoSQL style database
Member visibility is private, protected, public
class PersistentMemoryDB extends InMemoryDatabase implements Persistable {
// ....
}
#13 – Generics in Typescript Classes
Continuing with the database example from episode 12 but want the type stored to not just be number. Make it flexible with generics.
<T, K>
TS #14 – Mapped Types in Typescript
function listenToObject<T>(obj: T, listeners): void {
}
using template literals and “Capitalize” utility type
type Listeners<Type> = {
[Property in keyof Type as `on${Capitalize<string & Property>}Change`]: (newValue: Type[Property]) => void
}
#15 – Readonly and Static in Typescript Classes
Public in the constructor
class Doggy {
constructor(public readonly name: string, public age: number) {
// ...
}
}
Statics: we can only have one “dog” list
class DogList {
private doggies: Doggy[] = []
static instance: DogList = new DogList()
private constructor() {}
}
DogList.instance
#16 – Abstract Classes in Typescript
Modeling street fighters
Abstract means you can never instantiate it directly
abstract class StreetFighter {
constructor() {}
move() {}
fight () {}
abstract getSpecialAttack(): string {}
}
#17 – Mixins in Typescript
Add functionality to classes that you may or may not control
functions creating a generic type
#18 – Conditional Types in Typescript
Example with fetch function, pokemon api
import node-fetch
couldn’t find declaration file. Go to a community written types package.
import @types/node-fetch
shows multiple ways to do the same thing
#19 – Utility Types in Typescript – Part 2
“Parameters”
ConstructorType
ReturnType
InstanceType