-
Notifications
You must be signed in to change notification settings - Fork 0
typescript v3
- References:
- Course website: https://www.typescript-training.com/course/fundamentals-v3
- Official TypeScript website: https://www.typescriptlang.org/
Syntactic superset of JavaScript
- Compiles to readable JavaScript
- Its goal is to add types to JavaScript
- Three parts:
- Language
- Language Server
- Compiler
-
Benefits:
-
Makes the author's intent more clear
E.g. This does not tell if it's numerical addition or string concatenation.function add(a, b) { return a + b;}
But this one does:
function add(a: string, b: string) { return a + b;}
-
Moves some errors from runtime to compile time
-
Autocomplete like the one we have in C#; Where you can get suggestions based on the behaviour of an abject
-
- Files:
-
package.json{ "devDependencies": { "typescript: "^4.5" }, "scripts": { "dev": "tsc --watch --preserveWatchOutput" } } -
tsconfig.json{ "compilerOptions": { "outDir": "dist", // Where to put the .ts files "target": "ES3", "declaration": true // Generate declaration files (.d.ts) }, "include": ["src"] // Which files to compile }
-
- Summary:
-
.ts: code that runs + type info-
.js: code that runs -
.d.ts: type info
-
If we set target as
ES2017, the .ts and .js files look almost identical, except for the types -
- With
let age = 6, TypeScript infers that age is a number (unlike C#)From this point on, the compiler won't let us do
age = "not a number; This is one example of what's TypeScript designed for -
const age = 6is not a number, but a literal type; Because:-
constcannot be reassigned -
numberis an inmutable value type, unlikearray, which is a value type you can push things to (bcs it's mutable)
-
-
anyis the most flexible type in TypeScript, like the vanilla JavaScript variable rulesWhatever type you like
|any is any! -
let endTime: Datehas a type annotation, to enforce types
When I define return types like this
function add(a: number, b: number): number {}, I state my intentions upfront; i.e. When we're declaring the function, and not when we're using it
- To type an car
Objectlike "2002 Toyota Corolla" we can do:{ make: string model: string year: number chargeVoltage?: number // Optional }
We can use an
if (typeof car.chargeVoltage !== "undefined") as a Type guard, and TypeScript compiler will know that inside that branch of code, chargeVoltage is a number
- The TypeScript compiler provides Excess property checking, to ensure we don't pass extra properties that will not be safely accessible from inside the function
- To declare dictionary we can use the Index signature:
{ const phones: { [k: string]: { country: string area: string number: string } | undefined // This prevents us from writing things like phones.fax... } = {} }
-
Array types are as easy as adding
[]after the array's member type, likestring[] -
Tuples can be defined like
let myCar: [number, string, number], where we define the type we expect for each position -
Union type is like OR for types. e.g.
function flipCoin(): "heads" | "tails" {}, orconst result: Error | { name: string; email: string; } // result.name will compile; But result.email will not, as email does not exist in Error // So, we can only access the guaranteed stuff, whether it's an Error or that specific Object
- We can use type guards to narrow down and increase specificity like:
if (result instanceOf Error) { // result.name will compile, but result.email will not... } else { // result.email will compile here! }
-
Discriminated or "tagged" union type: Combine the use of type guards and a discriminating key, to switch between different possibilities; Suppose we have a "tuple" return type like:
function maybeGetUserInfo(): | ["error", Error] | ["success", { name: string; email: string }] {} const outcome = maybeGetUserInfo(); if (outcome[0] === "error") { // outcome is an error... } else { // outcome is the user info object... }
Here TypeScript understands that if
outcomehas the "error" string on its first position, the tuple is definitely of type["error", Error]
- We can use type guards to narrow down and increase specificity like:
-
Intersection type is like AND for types; e.g.:
function makeWeek(): Date & { end: Date } { // do stuff... return { ...start, end }; // start is a Date, and end too... } const thisWeek = makeWeek(); thisWeek.toISOString(); thisWeek.end.toISOString();
-
Type aliases:
- Allow us to give more meaningful names to our types (semantic)
- Define types in a single place
- Use export/import
// types.ts export type UserContactInfo = { name: string email: string } //utilities.ts import {UserContactInfo } from "./types" function printContactInfo(info: UserContactInfo) {}
- Inheritance-like implementation, by combining a type alias and an intersection type: Combine existing types with new behavior
type SpecialDate = Date & { getReason(): string }
-
Interfaces: A way of defining an object type; An object type looks like a "class instance"
-
Inheritance:
-
extends: Describes inheritance between "like" things
- Classes can extend from other classes
- Interfaces can extend from other interfaces
-
implements: Describes inheritance between "unlike" things
- Classes can implement interfaces
interface AnimalLike { eat(food): void } class Dog implements AnimalLike { eat(food) {} bark() { return "woof" } }
Interfaces are a great way to define contracts between things
- Classes can implement interfaces
-
extends: Describes inheritance between "like" things
-
Interfaces in TypeScript are open:
interface AnimalLike { isAlive(): boolean } function feed(animal: AnimalLike) { } // Second declaration with same name! interface AnimalLike { eat(food): void }
We could "augment" the global Window interface like this:
window.document // existing window.exampleProperty = 42 // new interface Window { exampleProperty: number }
-
-
Recursive types:
type NestedNumbers = number | NestedNumbers[] const val: NestedNumbers = [3, 4, [5, 6, [7]], 221]
- Exercise / example on how to describe JSON using types:
// @errors: 2578 type JSONObject = { [k: string]: JSONValue } type JSONArray = JSONValue[] type JSONValue = number | string | boolean | null | JSONObject | JSONArray ////// DO NOT EDIT ANY CODE BELOW THIS LINE ////// function isJSON(arg: JSONValue) { } // POSITIVE test cases (must pass) isJSON("hello") isJSON([4, 8, 15, 16, 23, 42]) isJSON({ greeting: "hello" }) isJSON(false) isJSON(true) isJSON(null) isJSON({ a: { b: [2, 3, "foo"] } }) // NEGATIVE test cases (must fail) // @ts-expect-error isJSON(() => "") // @ts-expect-error isJSON(class { }) // @ts-expect-error isJSON(undefined) // @ts-expect-error isJSON(new BigInt(143)) // @ts-expect-error isJSON(isJSON)
-
Call signatures:
// With interfaces... interface TwoNumberCalculation { (x: number, y: number): number } // With types... type TwoNumberCalc = (x: number, y: number) => number const add: TwoNumberCalculation = (a, b) => a + b const subtract: TwoNumberCalc = (a, b) => a - b
The return value of a void function is intended to be ignored; On the other hand, if we use
undefinedas a return type instead, then the compiler will specifically expect anundefinedto be returned -
Construct signatures: Define what should happen with the new keyword
interface DateConstructor { new (value: number): Date } let MyDateConstructor: DateConstructor = Date const d = new MyDateConstructor()
-
Function overloads: E.g. Type-check calls to the
handleMainEventfunction, which supports exactly two overloads:type FormSubmitHandler = (data: FormData) => void // Handler for the form type MessageHandler = (evt: MessageEvent) => void // Handler for the iFrame // Without Function overloads: A single implementation that handles all argument types... function handleMainEvent( elem: HTMLFormElement | HTMLIFrameElement, handler: FormSubmitHandler | MessageHandler ) // With Function overloads: One head per each arguments valid combination, and a single generic implementation... function handleMainEvent( elem: HTMLFormElement, handler: FormSubmitHandler ) function handleMainEvent( elem: HTMLIFrameElement, handler: MessageHandler ) function handleMainEvent( elem: HTMLFormElement | HTMLIFrameElement, handler: FormSubmitHandler | MessageHandler ) { if (typeof elem == HTMLFormElement) { } else { } }
-
thistypes: Instead of havinganyas the type ofthis, we can define a type for it when defining a function:function myClickHandler( this: HTMLButtonElement, event: Event ) {} // Once `this` type is defined, the function must be called using `call`... myClickHandler.call(myButton, new Event("click"));
- On top of JavScript classes, TypeScript allows us to declare the members of the class, with its types:
class Car { public make: string public model: string #year: number // The "#" symbol is from ECMAScript's private identifier constructor(make: string, model: string, year: number) { this.make = make; this.model = model; this.year = year; } }
- Access modifiers:
- public: everyone (default)
- protected: instance and subclasses
- private: instance only
"private" provides compile-time encapsulation; While "#" brings runtime encapsulation, which makes it harder to get access to Limited exposure is a technique where we take sth that is hidden, and provide controlled access to it; Like exposing a private thing through a protected method
- Param properties: access modifiers before constructor arguments
- Implicitly defines class members
- Can significantly reduce the
Cardeclaration code:class Car { constructor(public make: string, public model: string, public year: number) {} }
- Types describe a set of allowed values that a value might be; E.g.:
let a: 5 | 6 | 7 // anything in { 5, 6, 7 } let b: null // anything in { null } let c: boolean // anything in { true, false }
-
Top types describe the most general thing that exists in this type system:
any-
unknown, unlikeany, cannot be used without first applying a type guard
A practical use of top types is when converting a project from JavaScript to TypeScript
-
Bottom types describe no possible value; Like saying "you can pick anything you wish from this empty box"
-
never, where the only thing that can fit into a never is a never - A practical use of
neveris for implementing Exhaustive conditionals (enforced in Rust, BTW):// 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}` ) // Argument of type 'Boat' is not assignable to parameter of type 'never' }
-
- Besides
instanceof, TypeScript's built-in type guards also includetypeofandArray.isArray -
User-defined type guards: We're telling TypeScript to follow our instructions when it comes to evaluate the type of a variable
interface CarLike { make: string model: string year: number } let maybeCar: unknown // the guard function isCarLike( valueToTest: any ): valueToTest is CarLike { // key syntax here is `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 let maybeCar: CarLike }
- Nullish values are:
- null: there is a value, and that value is nothing
- undefined: the value isn't available (yet?)
- void: the return value of the function should be ignored
-
Non-null assertion operator: Tells TypeScript to ignore the possibility of a value to be null or undefined; If that's the case, it'll throw a runtime error!
cart.fruits!.push({ name: "kumkuat", qty: 1 })
-
Definite assignment operator:
!:operator is used to supresses TypeScript's objections about a class field being used, when it can't be proven that it was initialized
- A way of creating types that are expressed in terms of other types
- Example: Converting a "list of things" into a "dictionary of things"
// Dictionary definition const phones: { [k: string]: { customerId: string areaCode: string num: string } } = {} // "List of things" const phoneList = [ { customerId: "0001", areaCode: "321", num: "123-4566" }, { customerId: "0002", areaCode: "174", num: "142-3626" }, { customerId: "0003", areaCode: "192", num: "012-7190" }, { customerId: "0005", areaCode: "402", num: "652-5782" }, { customerId: "0004", areaCode: "301", num: "184-8501" }, ] // We'd like to convert it into a "Dictionary of things"... const phoneDict = { "0001": { customerId: "0001", areaCode: "321", num: "123-4566", }, "0002": { customerId: "0002", areaCode: "174", num: "142-3626", }, /*... and so on */ } // Implementation using `any`, where we lose the type information... function listToDict( list: any[], idGen: (arg: any) => string // To obtain a “key” from each item in the list ): { [k: string]: any } { const dict: { [k: string]: any } = {} list.forEach((element) => { const dictKey = idGen(element) dict[dictKey] = element }) return dict } const dict = listToDict( [{ name: "Mike" }, { name: "Mark" }], (item) => item.name ) console.log(dict) dict.Mike.I.should.not.be.able.to.do.this.NOOOOOOO // We need a way to define the relationship between the type we receive and the type we return, and this is what generics is about... function listToDict<T>( // T is a "type parameter" list: T[], idGen: (arg: T) => string ): { [k: string]: T } { // It auto-detects what T should be, and returning the right thing to us... const dict: { [k: string]: T } = {} list.forEach((element) => { const dictKey = idGen(element) dict[dictKey] = element }) return dict }
- Exercise / example on implementing map, filter and reduce for dictionaries:
///////////////////////////////////////// /////////// TESTING UTILITIES /////////// //////// no need to modify these //////// ///////////////////////////////////////// // @errors: 7006 7006 7006 7006 7006 console.clear() function assertEquals<T>( found: T, expected: T, message: string ) { if (found !== expected) throw new Error( `❌ Assertion failed: ${message}\nexpected: ${expected}\nfound: ${found}` ) console.log(`✅ OK ${message}`) } function assertOk(value: any, message: string) { if (!value) throw new Error(`❌ Assertion failed: ${message}`) console.log(`✅ OK ${message}`) } /// ---cut--- ///// SAMPLE DATA FOR YOUR EXPERIMENTATION PLEASURE (do not modify) const fruits = { apple: { color: "red", mass: 100 }, grape: { color: "red", mass: 5 }, banana: { color: "yellow", mass: 183 }, lemon: { color: "yellow", mass: 80 }, pear: { color: "green", mass: 178 }, orange: { color: "orange", mass: 262 }, raspberry: { color: "red", mass: 4 }, cherry: { color: "red", mass: 5 }, } interface Dict<T> { [k: string]: T } // Array.prototype.map, but for Dict function mapDict<T, U>( dict: Dict<T>, fn: (obj: T, name: string) => U): Dict<U> { let toReturn: Dict<U> = {}; for (const key in dict) { toReturn[key] = fn(dict[key], key); } return toReturn; } // Array.prototype.filter, but for Dict function filterDict<T>( dict: Dict<T>, fn: (obj: T) => boolean): Dict<T> { let toReturn: Dict<T> = {}; for (const key in dict) { if (fn(dict[key])) { toReturn[key] = dict[key]; } } return toReturn; } // Array.prototype.reduce, but for Dict function reduceDict<T, V>( dict: Dict<T>, fn: (currentValue: V, item: T) => V, initialValue: V): V { let returnValue = initialValue; for (const key in dict) { returnValue = fn(returnValue, dict[key]); } return returnValue; } ///////////////////////////////////////// ///////////// TEST SUITE /////////////// //////// no need to modify these //////// ///////////////////////////////////////// // MAP const fruitsWithKgMass = mapDict(fruits, (fruit, name) => ({ ...fruit, kg: 0.001 * fruit.mass, name, })) const lemonName: string = fruitsWithKgMass.lemon.name // @ts-ignore-error const failLemonName: number = fruitsWithKgMass.lemon.name assertOk( fruitsWithKgMass, "[MAP] mapDict returns something truthy" ) assertEquals( fruitsWithKgMass.cherry.name, "cherry", '[MAP] .cherry has a "name" property with value "cherry"' ) assertEquals( fruitsWithKgMass.cherry.kg, 0.005, '[MAP] .cherry has a "kg" property with value 0.005' ) assertEquals( fruitsWithKgMass.cherry.mass, 5, '[MAP] .cherry has a "mass" property with value 5' ) assertEquals( Object.keys(fruitsWithKgMass).length, 8, "[MAP] fruitsWithKgMass should have 8 keys" ) // FILTER // only red fruits const redFruits = filterDict( fruits, (fruit) => fruit.color === "red" ) assertOk( redFruits, "[FILTER] filterDict returns something truthy" ) assertEquals( Object.keys(redFruits).length, 4, "[FILTER] 4 fruits that satisfy the filter" ) assertEquals( Object.keys(redFruits).sort().join(", "), "apple, cherry, grape, raspberry", '[FILTER] Keys are "apple, cherry, grape, raspberry"' ) // REDUCE // If we had one of each fruit, how much would the total mass be? const oneOfEachFruitMass = reduceDict( fruits, (currentMass, fruit) => currentMass + fruit.mass, 0 ) assertOk( redFruits, "[REDUCE] reduceDict returns something truthy" ) assertEquals( typeof oneOfEachFruitMass, "number", "[REDUCE] reduceDict returns a number" ) assertEquals( oneOfEachFruitMass, 817, "[REDUCE] 817g mass if we had one of each fruit" )
- To describe constraints on generics, we use
<T extends HasId>; What we're saying is "I can be given any T, but it has to at least meet this base requirement" - Just like function parameters, "inner scopes can see outer scopes", type params work a similar way