# TypeScript ###### tags: `typescript` ## What is TypeScript TypeScript is an **syntactic superset** of JavaScript. The Goal is add **Types** into JavaScript. - Compiles to readable JS - Three parts: Language, Language Server and Compiler - Kind of like a fancy linter ## Why need Types ```javascript= function add(a, b) { return a + b; } ``` ```javascript= function add(a, b, c = 0) { return a + b + c; } ``` ### Types make the author's intent more clear ```typescript= function add(a: number, b: number): number { return a + b; } ``` ### Move some kinds of errors from runtime to compile time. - Values that are null or undefined - Incomplete refactoring - Breakage around internal code contracts (e.g. an argument becomes required) ### Great code authoring experience ## TSC compiler ```json= { "devDependencies": { "typescript": "^4.3.2" }, "scripts": { "dev": "tsc --watch --preserveWatchOutput" } } ``` ```json=5 { "compilerOptions": { "outDir": "dist", "target": "ES3" }, "include": ["src"] } ``` ```json= { "compilerOptions": { "outDir": "dist", "target": "ES2015" }, "include": ["src"] } ``` ```json= { "compilerOptions": { "outDir": "dist", "target": "ES2017" }, "include": ["src"] } ``` Legacy Browser support pipeline: Typescript ES2017 => Babal `.d.ts` known as a declaration file. ```typescript= /** * Add three numbers * @param a first number * @param b second */ export declare function addNumbers(a: number, b: number): Promise<number>; ``` A good way to think of TS files: - `.ts` files contain both type information and code - `.js` files contain only code - `.d.ts` files contain only type information ## Variables and Values ```typescript= let age = 6; age = "not a number"; ``` TypeScript is able to infer that `age` is a number, based on the value as we are declaring. In TypeScript, variables are "born" with their types. ```typescript= const age = 6; ``` TS is able to make a more specific: - `const` variable declarations cannot be reassigned - the initial value assigned to `age` is a number, which is an immutable value. ### Literal Types The type `6` is called a **literal type**. ### Implicit `any` and type annotations ```typescript= const RANDOM_WAIT_TIME = Math.round(Math.random() * 500) + 500; let startTime = new Date(); let endTime; setTimeout(() => { endTime = 0; endTime = new Date(); }, RANDOM_WAIT_TIME); ``` `endTime` is "born" without a type, so it ends up being an implicit `any`. If we wanted more safety here, we could be add a **type annotation**: ```typescript= const RANDOM_WAIT_TIME = Math.round(Math.random() * 500) + 500; let startTime = new Date(); let endTime: Date; setTimeout(() => { // endTime = 0; endTime = new Date(); }, RANDOM_WAIT_TIME); ``` ## Function arguments and return values ```typescript= function add(a: number, b: number): number {} ``` This is great way for code authors to state their intentions up-front. **explicit return type** **TypeScript is not a replacement for unit test** ## Objects Object types are defined by: - The `names` of the properties that are (or may be) present. - The `types` of those properties ```typescript= let car = { make: "Toyota", model: "Corolla", year: 2002 } ``` ```typescript= { make: string, model: string, year: number } ``` ### Optional Properties ```typescript= { make: string, model: string, year: number, chargeVoltage?: number } ``` ```typescript= function printCar(car: { make: string, model: string, year: number, chargeVoltage?: number }) { let str = `${car.make} ${car.model} ${car.year}`; if (typeof car.chargeVoltage !== 'undefined') { str += `// ${car.chargeVoltage}`; } console.log(str); } ``` **Compare with this** ```typescript= { make: string, model: string, year: number, chargeVoltage: number | undefined } ``` ## Excess property checking ```typescript= function printCar(car: { make: string, model: string, year: number, chargeVoltage?: number }) { } printCar({ make: "Tesla", model: "Model 3", year: 2020, chargeVoltage: 220, color: "RED" // <==== Extra Property }) ``` **Compare with this** ```typescript= function printCar(car: { make: string, model: string, year: number, chargeVoltage?: number }) { } const myCar = { make: "Tesla", model: "Model 3", year: 2020, chargeVoltage: 220, color: "RED" // <==== Extra Property }; printCar(myCar); ``` ### Index signatures ```typescript= { [k: string]: { country: string, area: string, number: string } } ``` ### Array ```typescript= const fileExtensions = ["js", "ts"]; // => string[] ``` ### Tuples ```typescript= let myCar = [2002, "Toyota", "Corolla"]; // => (string | number)[] const [year, make, model] = myCar; ``` **Compare to** ```typescript= let myCar: [number, string, string] = [2002, "Toyota", "Corolla"]; const [year, make, model] = myCar; ``` **Consider** ```typescript= const numPair: [number, number] = [4, 5, 6]; // Error ``` ```typescript= const numPair: [number, number] = [4, 5]; numPair.push(6); numPair.pop(); numPair.pop(); numPair.pop(); ``` ## Structural vs Nominal Types ### What is type checking? attempts to asked the question of compatibility or type equivalence ```typescript= function foo(x) { } // TYPE CHECKING // Is `myValue` type-equivalent to what `foo` want to receive? foo(myValue) ``` ### Static vs Dynamic type-checking is performed at **compile time** or **runtime** **TypeScript's type system is static** ### Nominal vs structural Nominal type systems are care about NAMES. Such as `JAVA` Structural type systems are care about SHAPES. Such as `TypeScript` ```typescript= class Car { make: string model: string year: number isElectric: boolean } class Truck { make: string model: string year: number towingCapacity: number } const vehicle = { make: "Honda", model: "Accord", year: 2017, } function printCar(car: { make: string model: string year: number }) { console.log(`${car.make} ${car.model} (${car.year})`) } printCar(new Car()) // Fine printCar(new Truck()) // Fine printCar(vehicle) // Fine ``` ### Duck Typing *Duck typing gets its name from the "duck test"* "Duck typing" is usually used to describe dynamic type systems. ## Union and Intersection Union types can be described using the `|` (pipe) operator ```typescript= function flipCoin(): "heads" | "tails" { if (Math.random() > 0.5) return "heads"; return "tails"; } const outcome = flipCoin(); ``` ```typescript= function flipCoin(): "heads" | "tails" { if (Math.random() > 0.5) return "heads" return "tails" } function maybeGetUserInfo(): | ["error", Error] | ["success", { name: string; email: string }] { if (flipCoin() === "heads") { return [ "success", { name: "Mike North", email: "mike@example.com" }, ]; } else { return [ "error", new Error("The coin landed on TAILS :("), ]; } } const outcome = maybeGetUserInfo(); ``` ### Narrowing with type guards Type guards are expressions, which when used with control flow statement, allow us to have a more specific type for a particular value. ```typescript= const outcome = maybeGetUserInfo(); const [first, second] = outcome; if (second instanceof Error) { // In this branch of your code, second is an Error second } else { // In this branch of your code, second is the user info second } ``` ### Discriminated Unions ```typescript= const outcome = maybeGetUserInfo(); if (outcome[0] === "error") { // In this branch of your code, second is an Error outcome } else { // In this branch of your code, second is the user info outcome } ``` ### Intersection Types Intersection types in TypeScript can be described using the `&` (ampersand) operator. ```typescript= function makeWeek(): Date & { end: Date } { const start = new Date(); const end = new Date(start.valueOf() + ONE_WEEK); return { ...start, end }; } const thisWeek = makeWeek(); thisWeek.toISOString(); thisWeek.end.toISOString(); ``` ## Interfaces and Type Aliases ### Type Aliases - define a more meaningful name for the type - declare the particulars of the type in a single place - import and export this type from modules Using `TitleCase` to format the alias' name. ```typescript= export type UserContactInfo = { name: string, email: string } ``` We can only declare an alias of a given name once within a given scope. ```typescript= type UserContactInfo = { } // Error type UserContactInfo = { } ``` ```typescript= type UserInfoOutcomeError = ["error", Error] type UserInfoOutcomeSuccess = [ "success", { name: string; email: string } ] type UserInfoOutcome = | UserInfoOutcomeError | UserInfoOutcomeSuccess export function maybeGetUserInfo(): UserInfoOutcome { // implementation is the same in both examples if (Math.random() > 0.5) { return [ "success", { name: "Mike North", email: "mike@example.com" }, ] } else { return [ "error", new Error("The coin landed on TAILS :("), ] } } ``` ### Inheritance create type aliases that combine existing types with new behaviour by using Intersection (&) types. ```typescript= type SpecialDate = Date & { getReason(): string } const newYearsEve: SpecialDate = { ...new Date(), getReason: () => "Last day of the year", } newYearsEve.getReason ``` ### Interfaces An interface is a way of defining an object type. An "object type" can be thought of as, "an instance of a class could conceivably look like this". ```typescript= interface UserInfo { name: string email: string } ``` ### Inheritance **EXTENDS** ```typescript= interface Animal { isAlive(): boolean } interface Mammal extends Animal { getFurOrHairColor(): string } interface Dog extends Mammal { getBreed(): string } function careForDog(dog: Dog) { dog.getBreed } ``` **IMPLEMENTS** ```typescript= class LivingOrganism { isAlive() { return true } } interface AnimalLike { eat(food): void } interface CanBark { bark(): string } class Dog extends LivingOrganism implements AnimalLike, CanBark { bark() { return "woof" } eat(food) { consumeFood(food) } } ``` ### Open Interfaces ```typescript= interface AnimalLike { isAlive(): boolean } function feed(animal: AnimalLike) { animal.eat animal.isAlive } interface AnimalLike { eat(food): void } ``` ```typescript= window.document // an existing property window.exampleProperty = 42 interface Window { exampleProperty: number } ``` ### Recursion Types ```typescript= type NestedNumbers = number | NestedNumbers[] const val: NestedNumbers = [3, 4, [5, 6, [7], 59], 221]; ``` ## Functions ### Callable Types Both type aliases and interfaces offer the capability to describe call signatures ```typescript= interface TwoNumberCalculation { (x: number, y: number): number; } type TwoNumberCalc = (x: number, y: number) => number; const add: TwoNumberCalculation = (a, b) => a + b; const subtract: TwoNumberCalc = (x, y) => x - y; ``` ### Function overloads ```typescript= type FormSubmitHandler = (data: FormData) => void type MessageHandler = (evt: MessageEvent) => void function handleMainEvent( elem: HTMLFormElement, handler: FormSubmitHandler ) function handleMainEvent( elem: HTMLIFrameElement, handler: MessageHandler ) function handleMainEvent( elem: HTMLFormElement | HTMLIFrameElement, handler: FormSubmitHandler | MessageHandler ) {} const myFrame = document.getElementsByTagName("iframe")[0] const myForm = document.getElementsByTagName("form")[0] handleMainEvent(myFrame, (val) => { }) handleMainEvent(myForm, (val) => { }) ``` ### `this` types ```typescript= function myClickHandler( this: HTMLButtonElement, event: Event ) { this.disabled = true } ``` ## Classes ### Class Fields ```typescript= class Car { make: string model: string year: number constructor(make: string, model: string, year: number) { this.make = make this.model = model this.year = year } } ``` ### Access modifier keywords | keyword | who can access | | ---------- | --------------------------------------- | | public | everyone (this is the default) | | protected | the instance itself, and subclasses | | private | only the instance itself | ```typescript= class Car { public make: string public model: string public year: number protected vinNumber = generateVinNumber() private doorLockCode = generateDoorLockCode() constructor(make: string, model: string, year: number) { this.make = make this.model = model this.year = year } protected unlockAllDoors() { unlockCar(this, this.doorLockCode) } } class Sedan extends Car { constructor(make: string, model: string, year: number) { super(make, model, year) this.vinNumber this.doorLockCode } public unlock() { console.log("Unlocking at " + new Date().toISOString()) this.unlockAllDoors() } } ``` ### Private ```typescript= class Car { public make: string public model: string #year: number constructor(make: string, model: string, year: number) { this.make = make this.model = model this.#year = year } } ``` ### Readonly ```typescript= class Car { public make: string public model: string public readonly year: number constructor(make: string, model: string, year: number) { this.make = make this.model = model this.year = year } updateYear() { this.year++ } } ``` ### Param properties ```typescript= class Car { constructor( public make: string, public model: string, public year: number ) {} } ``` ## Top and Bottom Types ### Types describe sets of allowed values `{true, false}`. ```typescript= const x: boolean ``` `{y | y is a number}` ```typescript= const y: number ``` ### Top Types A top type is a type that describes **any possible value allowed by the system** `{x | x could be anything}` TypeScript provides two of these types: `any` and `unknown` #### any playing by the usual JavaScript rules It's important to understand that `any` is not necessarily a problem. sometimes it's exactly the right type to use for a particular situation. We can see here that `any` is not always a "bug" or a "problem" it just indicates maximal flexibility and the absence of type checking validation. #### unknown Like `any`, unknown can accept any value However, `unknown` is different from `any` in a very important way: **Values with an `unknown` type cannot be used without first applying a type guard** ```typescript= let myUnknown: unknown = 14 // This code runs for { myUnknown| anything } if (typeof myUnknown === "string") { // This code runs for { myUnknown| all strings } console.log(myUnknown, "is a string") } else if (typeof myUnknown === "number") { // This code runs for { myUnknown| all numbers } console.log(myUnknown, "is a number") } else { } ``` #### Practical use of top types if you convert a project from JavaScript to TypeScript A lot of things will be `any` until you get a chance to give them some attention. `unknown` is great for values received at runtime. By consumers of these values to perform some light validation before using them, errors are caught earlier, and can often be surfaced with more context. ### Bottom type: `never` A bottom type is a type that describes **no possible value allowed by the system** “any value from the following set: { } (intentionally empty)” ```typescript= class Car { drive() { console.log("vroom") } } class Truck { tow() { console.log("dragging something") } } type Vehicle = Truck | Car let myVehicle: Vehicle = obtainRandomVehicle() // The exhaustive conditional if (myVehicle instanceof Truck) { myVehicle.tow() // Truck } else if (myVehicle instanceof Car) { myVehicle.drive() // Car } else { // NEITHER! const neverValue: never = myVehicle } ``` **error subclass** ```typescript= class UnreachableError extends Error { constructor(_nvr: never, message: string) { super(message) } } // The exhaustive conditional if (myVehicle instanceof Truck) { myVehicle.tow() // Truck } else if (myVehicle instanceof Car) { myVehicle.drive() // Car } else { // NEITHER! throw new UnreachableError( myVehicle, `Unexpected vehicle type: ${myVehicle}` ) } ``` ## Type Guards and narrowing ### Built-in type guards ```typescript= let value: | Date | null | undefined | "pineapple" | [number] | { dateRange: [Date, Date] } // instanceof if (value instanceof Date) { value // Date } // typeof else if (typeof value === "string") { value // "pineapple" } // Specific value check else if (value === null) { value // null } // Truthy/falsy check else if (!value) { value // undefined } // Some built-in functions else if (Array.isArray(value)) { value // [number] } // Property presence check else if ("dateRange" in value) { value // { dateRange: [Date, Date]; } } else { value // never } ``` ### User-defined type guards **“Untruths” in your type guards will propagate quickly through your codebase and cause problems that are quite difficult to solve.** ```typescript= interface CarLike { make: string model: string year: number } let maybeCar: unknown // the guard function isCarLike( valueToTest: any ): valueToTest is CarLike { return ( valueToTest && typeof valueToTest === "object" && "make" in valueToTest && typeof valueToTest["make"] === "string" && "model" in valueToTest && typeof valueToTest["model"] === "string" && "year" in valueToTest && typeof valueToTest["year"] === "number" ) } // using the guard if (isCarLike(maybeCar)) { maybeCar } ``` #### Asserts value is Type ```typescript= interface CarLike { make: string model: string year: number } let maybeCar: unknown // the guard function assertsIsCarLike( valueToTest: any ): asserts valueToTest is CarLike { if ( !( valueToTest && typeof valueToTest === "object" && "make" in valueToTest && typeof valueToTest["make"] === "string" && "model" in valueToTest && typeof valueToTest["model"] === "string" && "year" in valueToTest && typeof valueToTest["year"] === "number" ) ) throw new Error( `Value does not appear to be a CarLike${valueToTest}` ) } ``` ## Nullish ### `null` there is a value, and that value is nothing. ### `undefined` the value isn't available (yet) ### `void` a function's return value should be ignored ### Non-null assertion operator The non-null assertion operator (`!.`) is used to cast away the possibility that a value might be `null` or `undefined`. ```typescript= type GroceryCart = { fruits?: { name: string; qty: number }[] vegetables?: { name: string; qty: number }[] } const cart: GroceryCart = {} cart.fruits.push({ name: "kumkuat", qty: 1 }) cart.fruits!.push({ name: "kumkuat", qty: 1 }) ``` ### Definite assignment operator ```typescript= class ThingWithAsyncSetup { setupPromise: Promise<any> isSetup!: boolean constructor() { this.setupPromise = new Promise((resolve) => { this.isSetup = false return this.doSetup() }).then(() => { this.isSetup = true }) } private async doSetup() { } } ``` ## Generics `Generics` allow us to parameterize types What we need here is some mechanism of defining a relationship between the type of the thing we’re passed, and the type of the thing we’ll return. This is what Generics are all about ```typescript= function listToDict( list: any[], idGen: (arg: any) => string ): { [k: string]: any } { const dict: { [k: string]: any } = {} list.forEach((element) => { const dictKey = idGen(element) dict[dictKey] = element }) return dict } ``` ### Defining a type parameter Type parameters can be thought of as “function arguments, but for types”. Generics may change their type, depending on the type parameters you use with them. ```typescript= function listToDict<T>( list: T[], idGen: (arg: T) => string ): { [k: string]: T } { const dict: { [k: string]: T } = {} return dict } ``` ### Generic Constraints Generic constraints allow us to describe the “minimum requirement” for a type param ```typescript= function listToDict<T extends HasId>(list: T[]): Dict<T> { } ``` ### Scopes and TypeParams ```typescript= // outer function function tupleCreator<T>(first: T) { // inner function return function finish<S>(last: S): [T, S] { return [first, last] } } ``` <style> .markdown-body pre { background-color: #1E1E1E; border: 1px solid #555 !important; color: #73BCE0; border-radius:8px; /*border-radius:0px;*/ } .markdown-body pre .htmlembedded, .markdown-body pre .diff { color: #C8D4C8 !important; } .markdown-body pre .hljs-tag { color: #6D726E; } .markdown-body pre .token.keyword { color: #C586C0; } .markdown-body pre .token.string { color: #C68362; } .markdown-body pre .hljs-string { color: #C68362; } .markdown-body pre .hljs-comment, .markdown-body pre .token.comment { color: #6A9955; } .markdown-body pre .hljs-attr { color: #73BCE0; } .markdown-body pre .hljs-name { color:#569CD6; } .markdown-body pre .token.operator { color:#C8D4C8; background:transparent; } .markdown-body pre .token.property { color: #73BCE0; } .markdown-body pre .token.function { color: #DCDCAA; } .markdown-body pre .token.builtin { color: #34B294; } .markdown-body pre .token.number { color: #B5CEA8; } .markdown-body pre .token.constant { color: #3BC1FF; } .markdown-body pre .hljs-addition { color: #96D47D; background: #373D29; } .markdown-body pre .hljs-deletion { color: #E76A6A; background: #4B1818; } </style>