Se ha denunciado esta presentación.
Se está descargando tu SlideShare. ×

Type Driven Development with TypeScript

Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Anuncio
Próximo SlideShare
Embedded Android Workshop
Embedded Android Workshop
Cargando en…3
×

Eche un vistazo a continuación

1 de 84 Anuncio
Anuncio

Más Contenido Relacionado

Presentaciones para usted (20)

Similares a Type Driven Development with TypeScript (20)

Anuncio

Más de Garth Gilmour (20)

Más reciente (20)

Anuncio

Type Driven Development with TypeScript

  1. 1. © Instil Software 2020 Not Your Mothers TDD Type Driven Development in Typescript Richard Gibson Garth Gilmour
  2. 2. Garth (@GarthGilmour)
  3. 3. Richard (@rickityg)
  4. 4. TS is a superset of JavaScript • Created by Anders Hejlsberg Coding in TS enables you to: • Use the features defined in ES 2015+ • Add types to variables and functions • Use enums, interfaces, generics etc. Frameworks like Angular are built on TS • In particular its support for decorators • React etc. can benefit from TS The TypeScript Language TypeScript ES6 ES5
  5. 5. Things are about to get weird...
  6. 6. – Two types are identical if they have the same structure – Even if they are named and defined in unrelated places – In OO this means the same fields and methods – This is close to the concept of Duck Typing – Found in dynamic languages like Python and Ruby – But the compiler is checking your code at build time TypeScript is Structural not Nominal It’s all about the shape of the type...
  7. 7. type Pair = { first: string, second: number }; interface Tuple2 { first: string; second: number; } class Dyad { constructor( public first: string, public second: number) {} } An Example of Structural Typing Three types with the same shape
  8. 8. function test1(input: Pair) { console.log(`test1 called with ${input.first} and ${input.second}`); } function test2(input: Tuple2) { console.log(`test2 called with ${input.first} and ${input.second}`); } function test3(input: Dyad) { console.log(`test3 called with ${input.first} and ${input.second}`); } An Example of Structural Typing
  9. 9. export function structuralTyping() { let sample1 = { first: "wibble", second: 1234 }; let sample2 = new Dyad("wobble", 5678); test1(sample1); test1(sample2); test2(sample1); test2(sample2); test3(sample1); test3(sample2); } An Example of Structural Typing test1 takes a Pair test2 takes a Tuple2 test3 takes a Dyad
  10. 10. ------ Structural Typing ------ test1 called with wibble and 1234 test1 called with wobble and 5678 test2 called with wibble and 1234 test2 called with wobble and 5678 test3 called with wibble and 1234 test3 called with wobble and 5678
  11. 11. – Complex types can be declared via Type Aliases – Union Types specify a given type belongs to a set – Intersection Types combine multiple types together – String, boolean and number literals can be used as Literal Types The Weirdness Continues Some other fun features...
  12. 12. type MyCallback = (a: string, b:number, c:boolean) => Map<string, boolean> type MyData = [string, number, boolean]; function sample(func: MyCallback, data: MyData): Map<string, boolean> { return func(...data); } Type Aliases these are type aliases note the spread operator
  13. 13. export function showTypeAliases() { const data1: MyData = ["abc", 5, false]; const data2: MyData = ["def", 50, true]; const action:MyCallback = (p1, p2, p3) => new Map([ ["foo", p1 == "def"], ["bar", p2 > 10], ["zed", p3] ]); console.log(sample(action, data1)); console.log(sample(action, data2)); } Type Aliases compiler ensures correctness
  14. 14. Type Aliases
  15. 15. type Point = { x: number, y: number }; type RetVal = string | Point | Element; Union Types note the three options
  16. 16. function demo(input: number): RetVal { let output: RetVal = {x:20, y:40}; if(input < 50) { output = "qwerty"; } else if(input < 100) { output = document.createElement("div"); } return output; } Union Types might return Point might return string might return Node
  17. 17. export function showUnionTypes() { console.log(demo(40)); console.log(demo(80)); console.log(demo(120)); let result = demo(90); if(result instanceof Element) { result.appendChild(document.createTextNode("Hello")); console.log(result); } } Union Types no cast required
  18. 18. type Individual = { think: (topic: string) => string, feel: (emotion: string) => void }; type Machine = { charge: (amount: number) => void, work: (task: string) => boolean }; type Cylon = Individual & Machine; Intersection Types note the combination
  19. 19. const boomer: Cylon = { charge(amount: number): void {}, feel(emotion: string): void {}, think(topic: string): string { return "Kill all humans!"; }, work(task: string): boolean { return false; } }; Intersection Types
  20. 20. type Homer = "Homer"; type Simpsons = Homer | "Marge" | "Bart" | "Lisa" | "Maggie"; type Flintstones = "Fred" | "Wilma" | "Pebbles"; type Evens = 2 | 4 | 6 | 8 | 10; type Odds = 1 | 3 | 5 | 7 | 9; Type Literal Values assembling types from string literals assembling types from number literals
  21. 21. function demo1(input: Simpsons | Evens) { console.log(input); } function demo2(input: Flintstones | Odds) { console.log(input); } Type Literal Values Union Type built from Type Literals
  22. 22. export function showTypeLiterals() { demo1("Homer"); demo1(6); //demo1("Betty"); //demo1(7); demo2("Wilma"); demo2(7); //demo2("Homer"); //demo2(6); } Type Literal Values this is fine this is fine will not compile will not compile
  23. 23. The Warm-Up Is Over Now for the main event...
  24. 24. Mapped Types
  25. 25. – Mapped Types let us define the shape of a new type – Based on the structure of one or more existing ones – You frequently do this with values at runtime – E.g. staff.map(emp => { emp.salary, emp.dept }) – Consider the ‘three tree problem’ – Where you need three views of the abstraction – For the UI, Problem Domain and Database Mapped Types Defining new types based on old
  26. 26. type InstilReadOnly<T> = { readonly [K in keyof T]: T[K]; }; type InstilMutable<T> = { -readonly [K in keyof T]: T[K]; }; type InstilPartial<T> = { [K in keyof T]?: T[K]; }; type InstilRequired<T> = { [K in keyof T]-?: T[K]; }; Creating Mapped Types new type based on T ...but all properties immutable new type based on T ...but all properties mutable new type based on T ...but all properties optional new type based on T ...but all properties mandatory
  27. 27. const person = new Person("Jane", 34, false); const constantPerson: InstilReadOnly<Person> = person; const mutablePerson: InstilMutable<Person> = constantPerson; person.name = "Dave"; //constantPerson.name = "Mary"; mutablePerson.name = "Mary"; Creating Mapped Types will not compile fine now
  28. 28. let partialCustomer: InstilPartial<Customer>; let fullCustomer: InstilRequired<Customer>; partialCustomer = person; partialCustomer = {name: "Robin"}; fullCustomer = { ...person, makeOrder() {} } Creating Mapped Types a person is a partial customer so is this object literal we can create a full customer
  29. 29. type Stringify<T> = { [K in keyof T]: string; }; type StringifyFields<T> = { [K in keyof T]: T[K] extends Function ? T[K] : string; }; Distinguishing Fields From Methods new type based on T ...but all properties are strings methods are now excluded
  30. 30. function testStringify() { const customer: Stringify<Customer> = { name: "Jason", age: "27", married: "true", makeOrder: "whoops" }; return customer; } Distinguishing Fields From Methods only fields should be strings
  31. 31. function testStringifyFields() { const customer: StringifyFields<Customer> = { name: "Jason", age: "27", married: "true", makeOrder() { console.log("Order placed by: ", this.name); } }; return customer; } Distinguishing Fields From Methods problem solved 
  32. 32. – We can also work with return types at compile time – Say we have a function which returns an A, B or C – Where A, B and C are completely unrelated types – We know we will be testing the type at runtime – We want the compiler to strongly type the return value – Based on what is known about the type when we do the return Compile Space Voodoo Pt.1a Strongly typing disparate outputs
  33. 33. export function showManagingReturnTypes() { const data1 = new Centimetres(1000); const data2 = new Inches(1000); //input was Centimetres so output is Inches const result1 = convert(data1).inYards(); //input was Inches so output is Centimetres const result2 = convert(data2).inMetres(); console.log("1000 centimetres is", result1.toFixed(2), "yards"); console.log("1000 inches is", result2.toFixed(2), "metres"); } Strongly Typing Outputs Inches returned Centimetres returned
  34. 34. type CentimetresOrInches = Centimetres | Inches; type CentimetresOrInchesToggle<T extends CentimetresOrInches> = T extends Centimetres ? Inches : Centimetres; Strongly Typing Outputs if T is Inches then CentimetresOrInchesToggle will be Centimetres (at compile time) and vice versa our function will return Centimetres or Inches
  35. 35. function convert<T extends CentimetresOrInches>(input: T): CentimetresOrInchesToggle<T> { if (input instanceof Centimetres) { let inches = new Inches(input.amount / 2.54); return inches as CentimetresOrInchesToggle<T>; } let centimetres = new Centimetres(input.amount * 2.54); return centimetres as CentimetresOrInchesToggle<T>; } Strongly Typing Outputs WAT? compiler is certain ‘input’ is Centimetres, so the return type should be inches compiler is certain ‘input’ is Inches, so the return type should be Centimetres
  36. 36. – The last demo could have been achieved with overloading – DOM coding is a more practical example of where it is useful – A call to ‘document.createElement’ returns a node – But it would be great to have stricter typing on the result – So (for example) you could only set ‘source’ on a Video Compile Space Voodoo Pt.1b A practical example of typing outputs
  37. 37. type HtmlElements = { "p": HTMLBodyElement, "label": HTMLLabelElement, "canvas": HTMLCanvasElement } type ResultElement<T extends string> = T extends keyof HtmlElements ? HtmlElements[T] : HTMLElement; Typing HTML Elements create a type mapping that associates HTML tags with the corresponding DOM types select the correct DOM Node type at compile time
  38. 38. function createElementWithID<T extends string> (name: T, id: string): ResultElement<T> { const element = document.createElement(name); element.id = id; return element as ResultElement<T>; } Typing HTML Elements returning a strongly typed result
  39. 39. export function showTypingHtmlElements() { const para = createElementWithID("p", "e1"); const label = createElementWithID("label", "e2"); const canvas = createElementWithID("canvas", "e3"); para.innerText = "Paragraphs have content"; label.htmlFor = "other"; canvas.height = 100; console.log(para); console.log(label); console.log(canvas); } Typing HTML Elements Results strongly typed
  40. 40. – We can extract the types of parameters at compile time – This gets a little weird – We can't iterate over the names of the parameters – So we need to resort to techniques like recursive types Compile Space Voodoo Pt.2 Working with parameters
  41. 41. type AllParams<T extends (...args: any[]) => any> = T extends ((...args: infer A) =>any) ? A : never; type FirstParam<T extends any[]> = T extends [any, ...any[]] ? T[0] : never; type OtherParams<T extends any[]> = ((...things: T) => any) extends ((first: any, ...others: infer R) => any) ? R : []; Working With Parameters WAT?
  42. 42. function demo(p1: string, p2: number, p3: boolean) { console.log("Demo called with", p1, p2, "and", p3); } export function showWorkingWithParameters() { const var1: AllParams<typeof demo> = ["abc", 123, false]; const var2: FirstParam<AllParams<typeof demo>> = ”def"; const var3: OtherParams<AllParams<typeof demo>> = [456, true]; demo(...var1); demo(var2, ...var3); } Working With Parameters
  43. 43. type AllParams<T extends (...args: any[]) => any> = T extends ((...args: infer A) =>any) ? A : never; Working With Parameters all the parameter types from a function
  44. 44. type FirstParam<T extends any[]> = T extends [any, ...any[]] ? T[0] : never; Working With Parameters the first parameter type from a function
  45. 45. type OtherParams<T extends any[]> = ((...things: T) => any) extends ((first: any, ...others: infer R) => any) ? R : []; Working With Parameters the other parameter types from a function
  46. 46. – Let’s try to implement Partial Invocation – This is where we take an N argument function and produce – A function that takes a single argument ...which returns a function that takes the other arguments ...which returns the result Compile Space Voodoo Pt.3 Typing Partial Invocation WAT?
  47. 47. function findAllMatches(regex: RegExp, source: string, output: Array<string>): Array<string> { let match: RegExpExecArray | null = null; while((match = regex.exec(source)) !== null) { output.push(match[0]); } return output; } Partial Invocation Applied
  48. 48. const regex = new RegExp("[A-Z]{3}","g"); const data1 = "abcDEFghiJKLmno"; const data2 = "ABCdefGHIkjlMNO"; const results1 = findAllMatches(regex, data1, []); const results2 = findAllMatches(regex, data2, []); console.log(results1); console.log(results2); Partial Invocation Applied note the duplication
  49. 49. const findThreeUppercase = partial(findAllMatches)(regex); const results3 = findThreeUppercase(data1, []); const results4 = findThreeUppercase(data2, []); console.log(results3); console.log(results4); Partial Invocation Applied duplication removed via partial invocation
  50. 50. Partial Invocation (Iteration 1)
  51. 51. type AnyFunc = (...args: any[]) => any; type PartiallyInvoke<T extends AnyFunc> = T extends ((...args: infer A) => infer R) ? ((first: FirstParam<A>) => (x: OtherParams<AllParams<T>>) => R) : never; Typing Partial Invocation first attempt at a return type, using ‘FirstParam’ and ‘OtherParams’
  52. 52. function partial<T extends AnyFunc>(func: T): PartiallyInvoke<T> { return ((first) => (...others) => func(first, ...others)) as PartiallyInvoke<T>; } Typing Partial Invocation standard JavaScript solution, but with strong typing added
  53. 53. function test1(x: string, y: number, z: boolean): string { console.log("Demo called with ", x, y, " and ", z); return "Foobar"; } function test2(x: number, y: boolean, z: string): number { console.log("Test 2 called with ", x, y, " and ", z); return 123; } Typing Partial Invocation
  54. 54. export function showPartialApplicationBroken() { const f1 = partial(test1); const f2 = partial(test2); const result1 = f1("abc")([123,true]); const result2 = f2(123)([false,"abc"]); console.log(result1); console.log(result2); } Typing Partial Invocation very close but not quite ... OtherParams produces a tuple
  55. 55. Typing Partial Invocation tuples appear as arrays at runtime ...the last parameter is undefined
  56. 56. This Was Slightly Frustrating...
  57. 57. Partial Invocation (Iteration 2)
  58. 58. type PartiallyInvoke<T extends AnyFunc> = T extends ((...args: infer A) => infer R) ? ((first: FirstParam<A>) => Remainder<T>) : never; function partial<T extends AnyFunc>(func: T): PartiallyInvoke<T> { return ((first) => (...others) => func(first, ...others)) as PartiallyInvoke<T>; } Typing Partial Invocation (Iteration 2) now using a ‘Remainder’ type
  59. 59. type Remainder<T extends AnyFunc> = T extends ((...args: infer A) => infer R) ? A extends [infer P1, infer P2] ? (x:P2) => R : A extends [infer P1, infer P2, infer P3] ? ((x:P2, y:P3) => R) : A extends [infer P1, infer P2, infer P3, infer P4] ? ((x:P2, y:P3, z:P4) => R) : never : never; Typing Partial Invocation (Iteration 2) WAT?
  60. 60. type Remainder<T extends AnyFunc> = T extends ((...args: infer A) => infer R) ? A extends [infer P1, infer P2] ? (x:P2) => R Typing Partial Invocation (Iteration 2) if the original function took two inputs then disregard the first and use the second
  61. 61. type Remainder<T extends AnyFunc> = T extends ((...args: infer A) => infer R) ? A extends [infer P1, infer P2] ? (x:P2) => R : A extends [infer P1, infer P2, infer P3] ? ((x:P2, y:P3) => R) Typing Partial Invocation (Iteration 2) if the original function took three inputs then disregard the first and use the others
  62. 62. type Remainder<T extends AnyFunc> = T extends ((...args: infer A) => infer R) ? A extends [infer P1, infer P2] ? (x:P2) => R : A extends [infer P1, infer P2, infer P3] ? ((x:P2, y:P3) => R) : A extends [infer P1, infer P2, infer P3, infer P4] ? ((x:P2, y:P3, z:P4) => R) Typing Partial Invocation (Iteration 2) extend as necessary
  63. 63. export function showPartialApplicationImproved() { const f1 = partial(test1); const f2 = partial(test2); const result1 = f1("abc")(123, true); const result2 = f2(123)(false, "abc"); console.log(result1); console.log(result2); } Typing Partial Invocation (Iteration 2)
  64. 64. This Made Me Happy...
  65. 65. – What we have been doing is ‘coding at compile time’ – We have been persuading the TypeScript compiler to create new types and make inferences for us at compile time – We have seen that we can make choices – Via the ternary conditional operator – There is no support for the procedural loops – However we can use recursion to iterate at compile time Compile Space Voodoo Pt.4 Recursive Types
  66. 66. Compile Space Voodoo Pt.4 We can use recursion to iterate at compile time!!!
  67. 67. type IncTable = { 0: 1; 1: 2; 2: 3; 3: 4; 4: 5; 5: 6; 6: 7; 7: 8; 8: 9; 9: 10 }; export type Inc<T extends number> = T extends keyof IncTable ? IncTable[T] : never; Recursive Types (Numbers) what do you think Inc<6> would be?
  68. 68. type DecTable = { 10: 9; 9: 8; 8: 7; 7: 6; 6: 5; 5: 4; 4: 3; 3: 2; 2: 1; 1: 0 }; export type Dec<T extends number> = T extends keyof DecTable ? DecTable[T] : never; Recursive Types (Numbers) what do you think Dec<5> would be?
  69. 69. export type Add<A extends number, B extends number> = { again: Add<Inc<A>, Dec<B>> return: A }[B extends 0 ? "return" : "again"] Recursive Types (Numbers) WAT? recursively increment A whilst also decrementing B until the latter is 0
  70. 70. export type Add<A extends number, B extends number> = { again: Add<Inc<A>, Dec<B>> return: A }[B extends 0 ? "return" : "again"] Recursive Types (Numbers) Add<5, 3> Add<Inc<5>, Dec<3>> Add<Inc<6>, Dec<2>> Add<Inc<7>, Dec<1>> Add<Inc<8>, Dec<0>> 8
  71. 71. type SampleList = [boolean, number, string]; export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never; export type Rest<T extends any[]> = ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [] Recursive Types (Lists) use the spread operator to extract the first list item use the spread operator and function declaration syntax to extract the remainder of the list
  72. 72. type SampleList = [boolean, number, string]; export type LengthByProperty<T extends any[]> = T['length'] export type LengthByRecursion<T extends any[], R extends number = 0> = { again: LengthByRecursion<Rest<T>, Inc<R>> return: R }[ T extends [] ? "return" : "again" ] Recursive Types (Lists) calculate the length of a list at compile time in two ways
  73. 73. export type Prepend<E, T extends any[]> = // assign [E, ...T] to U ((head: E, ...args: T) => any) extends ((...args: infer U) => any) ? U : never //never reached Recursive Types (Lists) use the spread operator and function declaration syntax to prepend a type
  74. 74. type SampleList = [boolean, number, string]; export type Reverse<T extends any[], R extends any[] = [], I extends number = 0> = { again: Reverse<T, Prepend<T[I], R>, Inc<I>> return: R }[ I extends LengthByRecursion<T> ? "return" : "again" ] Recursive Types (Lists)
  75. 75. export function showRecursiveTypesWithLists() { type SampleList = [boolean, number, string]; const data1: Head<SampleList> = true; const data2: Rest<SampleList> = [12, "abc"]; const data3: LengthByRecursion<SampleList> = 3; const data4: Reverse<SampleList> = ["def", 123, false]; console.log(data1); console.log(data2); console.log(data3); console.log(data4); } Recursive Types (Lists)
  76. 76. Applying Recursive Types
  77. 77. https://www.youtube.com/watch?v=GFcQSQboBsM
  78. 78. Conclusions
  79. 79. We’ve Been Coding In Type Space
  80. 80. – In Test Driven Development we work from the outside in – The tests cannot write the implementation on our behalf – But they constrain our choices and point us the right way – Type Driven Development works the same way – We are not doing strong typing just to catch errors – Instead the compiler guides us to the right solution What is Type Driven Development? ...and why should you try it?
  81. 81. https://bitbucket.org/instilco/nidc-october-2020 https://www.youtube.com/watch?v=GFcQSQboBsM Examples from this slide deck The coding demo
  82. 82. Questions?

×