From 7b8bb8621396b680a3bdacbe8c10e79a0704af59 Mon Sep 17 00:00:00 2001 From: Sam Nicholson Date: Thu, 5 May 2022 15:46:08 +0100 Subject: [PATCH] Adding a stab at some notes for extra topics Notes for topics added to a WIP directory for now rather than force them into a structure and then have to redo it later. --- sessions/TypeScript-100/wip/custom_types.ts | 91 ++++++++++++++ sessions/TypeScript-100/wip/function_types.ts | 27 +++++ sessions/TypeScript-100/wip/generics.ts | 102 ++++++++++++++++ .../TypeScript-100/wip/index_signatures.ts | 82 +++++++++++++ sessions/TypeScript-100/wip/interfaces.ts | 111 ++++++++++++++++++ .../TypeScript-100/wip/type_assertions.ts | 53 +++++++++ .../TypeScript-100/wip/type_declarations.ts | 26 ++++ sessions/TypeScript-100/wip/type_inference.ts | 37 ++++++ 8 files changed, 529 insertions(+) create mode 100644 sessions/TypeScript-100/wip/custom_types.ts create mode 100644 sessions/TypeScript-100/wip/function_types.ts create mode 100644 sessions/TypeScript-100/wip/generics.ts create mode 100644 sessions/TypeScript-100/wip/index_signatures.ts create mode 100644 sessions/TypeScript-100/wip/interfaces.ts create mode 100644 sessions/TypeScript-100/wip/type_assertions.ts create mode 100644 sessions/TypeScript-100/wip/type_declarations.ts create mode 100644 sessions/TypeScript-100/wip/type_inference.ts diff --git a/sessions/TypeScript-100/wip/custom_types.ts b/sessions/TypeScript-100/wip/custom_types.ts new file mode 100644 index 00000000..fb1c4767 --- /dev/null +++ b/sessions/TypeScript-100/wip/custom_types.ts @@ -0,0 +1,91 @@ +/* + Type Aliases + + You can create your own custom types and then combine these to allow reuse. + + (this example uses literal types to limit the values to those defined) +*/ +type Flavoured = { flavour: ("Strawberry" | "Chocolate" | "Banana") }; +type Sized = { size: ("S" | "M" | "L") }; +type Scooped = { scoops: (1 | 2 | 3) }; + +/* + The & syntax creates a type which is the intersection of two existing ones. +*/ +type Milkshake = Flavoured & Sized; +let milkshake: Milkshake = { + flavour: "Strawberry", + size: "L" +} + +let brokenMilshake: Milkshake = { // error: missing required property from Sized + flavour: "Chocolate" +} + +/* + The | syntax creates a type which is the union of two existing ones. +*/ +type IceCream = Flavoured & ( Sized | Scooped ); +let dessert: IceCream = { + flavour: "Chocolate", + scoops: 3 +}; + +let strangeDessert: IceCream = { + flavour: "Banana", + size: "S" + // metaphor breaks down here since you can say: + , scoops: 3 + // which would make little sense. +} + +/* + All of these are Types and so they don't exist at runtime - `typeof` and + `instanceof` checks won't work with them. +*/ +console.log(typeof dessert); // object +console.log(dessert instanceof IceCream); // Compile error: IceCream is a type not a value. + +/* + If a type needs to have some optional properties, this is supported with + the `name?: type` syntax. + + You can also add readonly fields with the `readonly` keyword. +*/ +type Customer = { + name: string, + email: string, + mobile: string, + landline?: string // because it's the 21st century + readonly dateOfBirth: Date +}; + +let c: Customer = { + name: "Frank", + email: "frank@example.com", + mobile: "3482398472", + dateOfBirth: new Date("1970-01-01T00:00:00.000Z") +} + +c.dateOfBirth = new Date(); // can't edit a readonly field + +/* + As well as declaring variables of your types, you can create classes + which extend them. +*/ +class CustomerObj implements Customer { + name: string; + email: string; + mobile: string; + landline?: string; + dateOfBirth: Date; + + constructor (name: string, email: string, mobile: string, dob: Date) { + this.name = name; + this.email = email; + this.mobile = mobile; + this.dateOfBirth = dob; + } +} + +let cObj: Customer = new CustomerObj("name", "email", "mobile", new Date()); \ No newline at end of file diff --git a/sessions/TypeScript-100/wip/function_types.ts b/sessions/TypeScript-100/wip/function_types.ts new file mode 100644 index 00000000..506750cc --- /dev/null +++ b/sessions/TypeScript-100/wip/function_types.ts @@ -0,0 +1,27 @@ +/* + Function Types + + Since functions are first class in JS you can create function variables, + and TypeScript supports this by letting you define function types: + + (param: type, param: type) => type +*/ + +let add: (a: number, b: number) => number; +add = (a, b) => a + b; + +function combineNumbers( + x: number, + y: number, + combiner: (a: number, b: number) => number +): number { + return combiner(x, y); +} + +console.log( combineNumbers(10, 20, add) ); + +console.log( + combineNumbers(10, 20, + (a, b) => a * b // types inferred as numbers + ) +); \ No newline at end of file diff --git a/sessions/TypeScript-100/wip/generics.ts b/sessions/TypeScript-100/wip/generics.ts new file mode 100644 index 00000000..99569ced --- /dev/null +++ b/sessions/TypeScript-100/wip/generics.ts @@ -0,0 +1,102 @@ +/* + Generics + + Generics give us a way to write a function which can operate on any + type but still gives us type safety. + + For example the following function returns a random element from an + array of numbers: +*/ +function getRandomNumberElement(items: number[]): number { + let randomIndex = Math.floor(Math.random() * items.length); + return items[randomIndex]; +} +let numbers = [1, 5, 7, 4, 2, 9]; +let randomNumber: number = getRandomNumberElement(numbers); + +// But how can we change this to support strings as well? +let strings = ["a", "b", "c"]; +let randomString: string; +// randomString = getRandomNumberElement(strings); // won't compile + +/* + We could write the same function again with the types as string[] and + string, but this adds duplicated code. + + We could change the existing function to use the any type: +*/ +function getRandomAnyElement(items: any[]): any { + let randomIndex = Math.floor(Math.random() * items.length); + return items[randomIndex]; +} +let x: any = getRandomAnyElement(strings); // typeof x is any + +/* + But now we've lost our type information and we'd need to force the any + back into a type we know to use it safely. + + This would be simple enough here, but could get very fiddly with more + complicated functions and would add a lot of verbose casts. + + The solution is to make the function Generic. + This preserves the type safety of our function and allows easy reuse. +*/ +function getRandomElement(items: T[]): T { + let randomIndex = Math.floor(Math.random() * items.length); + return items[randomIndex]; +} +let y: number = getRandomAnyElement(numbers); +let z: string = getRandomElement(strings); + +/* + Generics can allow you to limit the arguments to a function based on + inheritance. +*/ +function displayPerson(per: T): void { + console.log(`Person: ${per.name}, ${per.age}`); +} + +function displayName(named: T): void { + console.log(`Named: ${named.name}`); +} + +interface Named { + name: string +} +class Person implements Named { + name: string; + age: number; + + constructor (name: string, age: number) { + this.name = name; + this.age = age; + } +} + +let dee = new Person("Dee", 42); +displayPerson(dee); // Type safe operation a Person +displayName(dee); // Type safe operation a Named object + +/* + Generics can also be used to create general purpose classes. + + You can then reuse the class with a variety of types as needed, but without + resorting to passing around `any` variables and sacrificing type safety. +*/ +class Pair { + key: K; + value: V; + + constructor (key: K, value: V) { + this.key = key; + this.value = value; + } +} + +let pair = new Pair(1, "things"); +let k1: number = pair.key; +let v1: string = pair.value; + +let pair2 = new Pair(2, new Pair("nested", "pair")); +let k2: number = pair2.key; +let v2: Pair = pair2.value; diff --git a/sessions/TypeScript-100/wip/index_signatures.ts b/sessions/TypeScript-100/wip/index_signatures.ts new file mode 100644 index 00000000..cd43f2f6 --- /dev/null +++ b/sessions/TypeScript-100/wip/index_signatures.ts @@ -0,0 +1,82 @@ +/* + Index Signatures + + Index signatures in TypeScript give you a way of dealing with types where + you know the shape of the type, but you're not sure what all of the + properties will be. + This feature lets you keep your type safety when accessing the properties + of the object by index - without this, you'd get any back. +*/ +function averageGrades( + studentMarks: { + [index: string]: number + } +): number { + let total = 0, + count = 0; + for (const subject in studentMarks) { + let subjectMarks: number = studentMarks[subject]; + total += subjectMarks; + count++; + } + return (count > 0) ? total / count : 0; +} + +let studentMarks = { + science: 85, + maths: 35, + english: 45 +}; +let studentAverageMark: number = averageGrades(studentMarks); + +let studentMarks2 = { + name: "Charlie", + science: 25, + art: 95, + music: 105, +}; +let studentAverageMark2: number = averageGrades(studentMarks2); + +/* + You can achieve a similar thing using the Record utility type. +*/ +function averageGrades2( + studentMarks: Record +): number { + let total = 0, + count = 0; + for (const subject in studentMarks) { + let subjectMarks: number = studentMarks[subject]; + total += subjectMarks; + count++; + } + return (count > 0) ? total / count : 0; +} + +averageGrades2(studentMarks); +averageGrades2(studentMarks2); + +/* + You can use this as well as named properties. +*/ +interface Indexed { + id: number; + name: string; + [index: number]: string; +}; + +let x: Indexed = { + id: 1, + name: "thing", + 42: "test" +} +let y: string = x[42]; // type information provided by index signature. + +/* + This won't work if you declare that you're using string keys. + Then you're effectively giving conflicting type information. +*/ +interface BrokenIndexed { + id: number; + [index: string]: string; +} \ No newline at end of file diff --git a/sessions/TypeScript-100/wip/interfaces.ts b/sessions/TypeScript-100/wip/interfaces.ts new file mode 100644 index 00000000..bf8553da --- /dev/null +++ b/sessions/TypeScript-100/wip/interfaces.ts @@ -0,0 +1,111 @@ +/* + Interfaces + + Interfaces are veeery similar to Types shown above, effectively just + providing a more familar syntax around the same idea. +*/ +interface Flavoured { + flavour: ("Strawberry" | "Chocolate" | "Banana") +}; +interface Sized { + size: ("S" | "M" | "L") +}; +interface Scooped { + scoops: (1 | 2 | 3) +}; + +/* + Much like the & syntax from types you can create an intersection interface + using extends. +*/ +interface Milkshake extends Flavoured, Sized { +}; +let milkshake: Milkshake = { + flavour: "Strawberry", + size: "L" +} + +let brokenMilshake: Milkshake = { // error: missing required property from Sized + flavour: "Chocolate" +} + +/* + But there's no equivalent for the | syntax with interfaces. + This is because an interface needs to have a definite list of properties. +*/ +interface IceCream extends Flavoured, Sized, Scooped { +}; +let dessert: IceCream = { + flavour: "Chocolate", + scoops: 3 +}; + +/* + Again these are Types and so they don't exist at runtime so `typeof` and + `instanceof` checks won't work with them. +*/ +console.log(typeof dessert); // object +console.log(dessert instanceof IceCream); // Compile error: IceCream is a type not a value. + +/* + If a type needs to have some optional properties, this is supported with + the `name?: type` syntax. + + You can also add readonly fields with the `readonly` keyword. +*/ +interface Customer { + name: string, + email: string, + mobile: string, + landline?: string // because it's the 21st century + readonly dateOfBirth: Date +}; + +let c: Customer = { + name: "Frank", + email: "frank@example.com", + mobile: "3482398472", + dateOfBirth: new Date("1970-01-01T00:00:00.000Z") +} + +c.dateOfBirth = new Date(); // can't edit a readonly field + +/* + As well as declaring variables of your types, you can create classes + which extend them. +*/ +class CustomerObj implements Customer { + name: string; + email: string; + mobile: string; + landline?: string; + dateOfBirth: Date; + + constructor (name: string, email: string, mobile: string, dob: Date) { + this.name = name; + this.email = email; + this.mobile = mobile; + this.dateOfBirth = dob; + } +} + +let cObj: Customer = new CustomerObj("name", "email", "mobile", new Date()); + +/* + One thing that you can do with interfaces which you can't achieve with + types, is Declaration Merging - combining multiple interfaces with the + same name into a single one at runtime. + + With types you'd need to rename them and union them together. +*/ +interface Point { + x: number +}; +interface Point { + y: number +} + +let p: Point = { // Fields from both declarations + x: 10, + y: 10 +}; \ No newline at end of file diff --git a/sessions/TypeScript-100/wip/type_assertions.ts b/sessions/TypeScript-100/wip/type_assertions.ts new file mode 100644 index 00000000..ad3181ca --- /dev/null +++ b/sessions/TypeScript-100/wip/type_assertions.ts @@ -0,0 +1,53 @@ +/* + Type Assertions + + This is a way of forcing the compiler to treat a variable as a specific + type, i.e. overriding the type inference. + + This can be used to force an `any` into a known type to help catch errors, + e.g. +*/ +const json = `{ "color": "pink", "type": "fluff" }`; + +let obj = JSON.parse(json); // obj is `any` so we get no help from the compiler +console.log(obj.colour); // typo not caught by type checks. :( + +interface Thing { + type: string, + color: string +} +let typedObj = JSON.parse(json) as Thing; // type is now `Thing` +console.log(typedObj.colour); // so the compiler catches our bug + +/* + This seems quite similar to what you can achieve by just specifying the + types of your variables, but asserting is a little more flexible: +*/ +interface Person { + name: string; +} + +const dennis: Person = { // error - object literal specified unknown property + name: "Dennis", + email: "dennis@example.com" +}; +const mac = { // OK + name: "Mac", + email: "mac@example.com" +} as Person; + +/* + A word of warning though... + + You're effectively telling the compiler "I know what I'm doing, trust me", + this can easily lead to issues at runtime + + e.g. +*/ +function foo(bar: any): void { + let x = bar as string; + console.log(x.substring(0,3)); +} + +foo("things"); // Prints 'thi' +foo(123456); // TypeError as substring isn't a function of number. \ No newline at end of file diff --git a/sessions/TypeScript-100/wip/type_declarations.ts b/sessions/TypeScript-100/wip/type_declarations.ts new file mode 100644 index 00000000..de9449e5 --- /dev/null +++ b/sessions/TypeScript-100/wip/type_declarations.ts @@ -0,0 +1,26 @@ +/* + Type Declarations + + So we've seen how adding types to our code can help make things better, but + what about libraries? + + Having types for the parts of your application that aren’t your code will + greatly improve your TypeScript experience. + Where do these types come from? +*/ +let x = Math.max(1, 20, 8, 70); + +let y = Math.abs(1, 20, 8, 70); // error: Expected 1 arg but got 4 + +/* + How did typescript know? + + Ctrl+Click into the definition of Math.abs and you can see the types + which have been added as declarations for the standard Math + implementation: + + abs(x: number): number; + + Most modules will export a `.d.ts` type declaration file which makes + working with that library in typescript much easier. +*/ \ No newline at end of file diff --git a/sessions/TypeScript-100/wip/type_inference.ts b/sessions/TypeScript-100/wip/type_inference.ts new file mode 100644 index 00000000..feda127b --- /dev/null +++ b/sessions/TypeScript-100/wip/type_inference.ts @@ -0,0 +1,37 @@ +/* + Type Inferrence + + When you don't explicitly define the type of a variable, TypeScript will try + to figure out the type of the variables enforce type checks. +*/ +let n: number = 50; +let halfN = n / 2; // number is inferred from the type of n + +let s = "test"; // string is inferred from the initial value +let halfS = s / 2; // error: string doesn't support this operation +s = 100; // error: number not assignable to string + +function foo(bar=1000): void { + console.log(Math.sqrt(bar)); // typeof bar is inferred from default value +} +foo("test"); // error: foo() expects a number + +let arr = [0, 1, 2, 3]; // number[] +let arr2 = [2, "things"]; // (number | string)[] +for (const e of arr2) { + let x = 2 * e; // error: string doesn't support this + let y = e.substring(0, 3); // error: number doesn't support this + + /* + Type guards can be used in these circumstances where a variable can be + one of several types. + */ + if (typeof e === 'number') { + let z = 2 * e; // z is a number + console.log(z); + } else if (typeof e === 'string') { + let z = e.substring(0, 3); // z is a string + console.log(z); + } +} +