Not Your Mother's TDD: Type Driven Development in TypeScript - G Gilmour & R Gibson - NIDC2020

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
garth richards we'll look forward to your talk take it away right so welcome to the uh the first session of the day hopefully and uh this is not your mother's tdd so a type driven development and type script so we've got a lot of material to get through and very little time to do it so we'll just abbreviate the introductions so uh my name is garth gilmore and i'm the head of learning at instill and my name is richard gibson and i work mainly on devtools and aws at hex labs so as you know there are many languages these days that transpile to javascript and you've got a very very broad choice there so uh you've got reason ml uh you've got purescript you've got elmore that kind of thing but far and away the leading contender at the moment is typescript and the way typescript is introduced of course is that it's an extension to javascript and it brings the world of strong typing to javascript so you've got the addition of generics you've got interfaces you can give variables types you know all that kind of thing so everybody's familiar with typescript from that light but after that things get very very weird and that's what we're here to talk about today so typescript is a structural language it doesn't do nominal typing it does structural typing what does that actually mean well in nominal typing let's say you've got a type called employee and a type called member of staff well those are two completely different types even if they have precisely the same structure you know even if they have the same fields and methods and so on uh but in structural typing uh they actually count as the same type because they have the uh the same internal structure so structural typing is where two types are identical even if they have the same structure and of course you may have heard this before this is the kind of thing that you have with duct typing in ruby and python but here we're talking about these checks these structural checks being enforced at build time and not at run time so here's a little example of structural typing here so we've got three types we've got a type called pair we've got a type called tuple2 and we've got a type called dyad and if you look at them you'll see that they all have the same shape okay so in nominal typing these would be three completely different types yeah in structural typing they're all pretty much the same because they all have the same internal structure and then you see here we can declare some test methods so we've got a test method that takes a pair we've got a test method that takes a tuple we've got a test method that takes a diad yeah but if i go out and try and call them well you see here i'm passing in an object literal and i'm passing in a dyad object but i can call test one which is declared to take a pair and it works i can call test2 which is declared to take a tuple two and it works i can call test three which is declared to take a diad and it works you know so uh all of these calls will succeed whether we're passing in a dyad object or an object literal or whatever because they have the same structure okay so uh whenever we run it it works just fine okay so that gives you a little bit of an insight into what structural typing is but we're only getting started okay you'll find that we're going to say that an awful lot in this talk you know so this is only the beginning there's lots of goodness layered on top of that which uh richard will now take us to uh to level two okay so for our next level we're just going to talk about a few of the basic tools oh thanks cars um that typescript gives us when we're working with structured types so we'll start with some stuff to do with hyperlaces uh then talk about some union types intersection types and then literal types so type aliases so if you think about when you're using a constant in value programming or runtime programming to be because you want to not repeat something uh repeat some sort of value well the same idea can be made with types as well and this is something that's in a number of languages so instead of having to write the same type signature in numerous different places we can actually assign this to an alias and then use that throughout our code just for uh just assuming that we're only having the update in one place so as you can see in this example we've got a type alias my callback and that's for a function that will take a string number and billion and then return a map of string and booleans then we have another type alias mydata which is just a tuple type or a list that can a list but it's a specific type of list that can only take three values a string a number and a boolean in that order and so you can see here we've got a function of sample we don't have to go and write the full callback signature here we're able to just use our my callback type alias use our our my data type list as well as the parameters and then what we can do with that so you can see these are the typolises what you can do with that my data there that we've sent in that triples of three we can use a spread operator and that will be used as the parameters for the function and so what happens here is if we were to my data would say a string a string and a boolean well the compiler would catch this and wouldn't let this i wouldn't let that pass so you see where with our example here where we are going creating our my data and we've got again the compiler is enforcing that it's a string followed by a number followed by a boolean and that we're able to then make a function that takes those three parameters and creates a map and as you can see whenever we when we do this the compiler ensures all the correctness and when we run it it runs as as planned okay so next union types so union types are where we want to take specific uh just separate types that are unrelated and be able to make a new type that can be any of those separate types so for example in this example we've got one called a return value which could be a string or this type point that we've created or say a dom element so and then when we go to use this we can use it and we've got here a function that will return one of these types so what we're doing is we're taking a number and based on what that number is we will return a different type here so if the number is less than 50 we're going to return a string if it's less than 100 we're going to return a dom element otherwise we're returning our type that point at the point type that we created and here we go here's where soon okay and then whenever we actually go to use this we can see that if we can actually uh use our instance of a keyword to check what the element is to see what type that that instance is and when we do this um typescript is able to say that if we have a dom element it's actually able to access those methods without having to explicitly cast so it's like a smart cast that's able to be used so it the compiler has doing a lot of work for you to to catch uh to catch those sort of tight to ensure that you've got the correct types in the correct places and that the right methods are available or functions are available on those so intersection types this is where we're able to join two existing types together to make a new one so if you know anything about your battlestar galactica you'll know a cylon is both an individual and a machine so it means that it has all the functionality of an individual and of a machine and you can see that when we use this amphisound we combine the two together to make our new type now if we're going to go and actually instantiate one of those types that cyclon you can see that we have all of uh the pro the functions that were declared as a machine and an individual so it must have the charge feel think and work also though now a cylon can be used anywhere where we where a machine is look as expected or where an individual is expected so if you have a parameter that takes an individual this boomer can be used at that place okay type literals i guess the easiest way to say what a type literal is is to say that it is literally only one it can only be assigned to one value so for example with first one here we've got a type that's homer that's a string literal which means that the type we so we have done nothing here at runtime here but what we've done is we've created a type that can only be this string homer it sounds a bit strange why would you actually want to use this but if we we can use this with other things so if you can see here we're combining it with our union types and when we do this we can create a type called simpsons which can be homer marge barts lisa or maggie or the flintstones we can do this with booleans as well which i haven't got here but or we can do it with with numbers so if we want to just create a closed set of numbers say our even digits or even numbers 2 4 6 8 10 or or odd numbers then we can combine we can combine again so where would we use these so say we're using this and we've got now you've got two demo functions where they're taking uh one takes simpsons or an even number as the inputs and one takes the flintstones or the odds so what what can we do with this well we go to run these we can see that if we use demo1 with homer or with an even number we'll see that the compiler allows this to happen or demo 2 with wilma or an odd number that that also was allowed but these are fine but whenever we try to run demo one with a flintstone or with an odd number we can't do this we'll get red lines the compiler won't allow this to pass and again demo two with it with homer will do the same back to you girth cool so uh this may seem like an awful lot to take in if you're meeting typescript for the first time but believe it or not that was just the warm-up you know so we're now on to level three you know we're now on to the uh the main event and this is something called mapped types so uh regular programming for the moment we're going to call programming in the world of values you know so we normally program with you know numbers and objects and so on but now we're going to be doing programming with types but in the world of values of course we're well used to the map operations so you could map over a list of employees to extract their salaries or to extract their salaries in their department something like that so we're well used to the idea of mapping over lists of objects at runtime what we're not used to is mapping over types at compile time but that's what typescript lets us do so a map type is when we have an existing type and then we're going to map over we're going to loop over the properties of that type in order to define a new one so to see why this is useful if you think about you know the canonical web application where you have the front end in the browser and then a sql database and you're inevitably pushed towards having three versions of each type um so let's say we have a customer type and whenever we're receiving the request over the web well the credit card number is just going to be a string but then in our canonical customer representation you know in our business logic we're going to have a credit card type and then whenever we go to push it to the database well it's a string again but now we want it to be a var chart so we want three versions maybe of the customer type uh only one in which the credit card is strongly typed in the others it's going to be either a string or of our chart so wouldn't it be nice if we could generate two versions of the type from the canonical one the one that's in our business logic so that's what map types give us so the syntax takes a while to get used to here but if you look you see we've got a type called instill read-only and what we're doing is we're mapping over the properties of t okay so whatever t is we're going to map over its properties and we're going to generate a new type and that new type will be the same as the existing one except if you look there we're adding the read-only keyword so everything's going to be uh read-only or we could make it mutable yeah so we could loop over the properties of t and we could say minus only so what that actually means is take it away if it's there or we could loop over all the properties of t and we could make it partial by adding the question mark or we could go through and we could say everything's required so whenever we say minus question mark we're saying if the optionality is there remove it so this is the idea of mapped types the idea that you can have an existing type call it t and then iterate over all the properties and add or remove things to create a new type and here's some demos of using it here so in every case we're going to have a person object yeah but the reference to it is going to be either a constant person or a mutable person using the types that we've just created yeah and if the reference is a constant person then we won't be allowed to change the thing whereas if the reference is mutable then we will and then uh same thing here where let's say we've got a customer type and we create an install partial of customer well if a person is a partial customer you know if it's a subset of a customer uh then we'll be able to do that right there and so on so you can see how we can use map types to generate one type from another but if you think about it you start getting into problems because properties include both fields and methods so let's say we define a stringify type so this is going to be a map type where the new type is going to be the same as whatever t was but everything's going to be of type string but unfortunately that includes both fields and methods yeah so what we want to do is we want to say no no no no it's only the fields should be strings yeah and leave the methods alone so uh map types can actually include conditionality and that's what we're doing here so you see here we're saying okay if the type of the current property extends function will then leave it alone or rather replace it with itself okay so effectively there'll be no change so functions will be left alone whereas fields will have their types change to string so if we use our stringify version we can see that it kind of works yeah but whenever it comes to what was a method we can assign it to a string whoops you know whereas if we use our stringify fields type well then it works just fine okay so uh this is a good example of why we need to have the ability to do conditionals whenever we're working with map types but that's just the beginning okay so uh we can take all of this as a foundation yeah and then start doing really interesting things we can start doing what some people would call type driven development other people would call type level programming you know we're uh we're programming in the compile space we're programming with types so here's a nice little example here so uh let me just see yeah so let's say what we want to do is we want to have a convert function and the idea is that if you call it and pass in centimeters then what you get back is inches and if you call it and pass in inches then what you get back is centimeters and this is all worked out at compile time in other words if i pass in a value in terms of centimeters when the the result should be strongly typed as inches so i should be able to call in yards safely and that should be determined at compile time whereas if i pass in inches then the result should come back in centimeters and on that i should be able to call in meters again this should all be worked out safely at compile time so how are we going to do this yeah well first of all as richard was showing us we can use our union types so we can define a type called centimeters or inches and this is either centimeters or inches yeah and then we can define what we've called a toggle type so uh a good way to understand this is that we're going to pass in a t and we're going to get back the reverse of t okay so uh if t is inches then centimeters or inches toggle will be centimeters yeah and vice versa and here's our little um implementation here so uh the thing to note is that we've got a guard yeah so we're checking there to see if the input is of type centimeters so if it is then the compiler has complete metaphysical certitude that input is centimeters so uh our toggle type uh that's going to resolve to be inches yeah and uh vice versa so if we pass in inches we'll get a strongly type result of centimeters if we pass in centimeters we'll get back a strongly type result of inches yeah and that's really really nice you know so that's something that has value yeah and that we've managed to add that into our type system on the fly um but that is slightly artificial because what we could have done is we could have achieved that with overloading so you know in the interest of simplicity and demoing the feature it's a little bit simple you know so we could achieve the same effect with overloading so let me just show you a really practical example so we're all familiar of course with programming in the browser and document create element so for example if we were to say document create element and ask for a paragraph or a video element or something like that we would get back a nude but let's say if we were to say document create element and say p well then we'd like something that we could set the the inner text of if we were to say document create element video well then we'd like something that we could set the the source of and so on so here's how we could do it here so what we're basically doing at the top there is we're creating a little map so we're associating the string name of an html tag with the corresponding dom node and then below we've got a type called result element of t so what this is going to do is effectively it's going to do a lookup in that table so t we've said there extends string so t is going to be some kind of string and we're saying there in the bottom line okay if that string is a key in the table we'll then return the corresponding html node type in other words if t is labeled well then we'd like an html label element please you know that's what we want the the result element of t to resolve as and if there's nothing in the table to match against it well then a good old html element uh that will do fine so what we can do here is we can have this little helper function create element with id which uses our result element type and it's going to return a strongly typed result for us so if we go on to the demo here you see i can call my function and say i want a paragraph or i want a label or i want a canvas and the results that come back will be strongly typed okay so i will be able to set properties specific to that particular node type and again none of this is happening at runtime this is all happening at compile time so the the compiler is helping us you know it's guiding us to the correct solution so this is a a practical example of doing type level development um and it just gets better from there yeah so uh we can go along yeah and we can actually work out the types of the parameters at runtime so here's a type that will resolve to all the parameters then the first parameter then the remaining parameters and i know you kind of look at this and you go what okay so uh let me just uh try and break it down a little bit more so let's say we've got a demo function and uh this takes three inputs and then we've got three variables here uh var one var2 and var three okay so you see there we can only set var1 to a tuple of all the parameters that the function takes we can only set var2 to a value of the same type as the first parameter that the function takes and we can only set var three yeah to be a tuple of the remaining parameters that the uh the function takes okay uh so uh that's uh that's quite impressive yeah and then uh we have the implementation here which is uh sorry it's a little bit uh tricky to talk through in the sunlight here but we can extract all the parameters we can extract the first parameter and we can extract the uh the remaining parameters so uh you might say that's a nice trick yeah but how would it actually be applied so um whenever i was learning this myself i kind of gave myself a graduation exercise which was trying to implement partial invocation so in case you're not familiar just let me quickly summarize what partial invocation is so here we've just got a standard javascript function where we're going to take a regular expression and we're going to take some text and we're going to find all the matches for the regular expression in the text and we're going to try to add them to an output array so we'll pass in an array all the matches we find will be pushed on to that array and then it's that array that's returned so let's say we've got a regular expression and we've got two strings that we want to search you know so uh this is how we could use it here and that would work just fine but if you look uh we're duplicating the uh the regular expression so what we could do with partial invocation is we could say okay i wish to partially apply find all matches and the way i've written it is that we'll get back a function that takes a single input and then if you call that that will return a function that takes the remaining inputs and then that's the thing we're actually going to call so you see here find three uppercase is going to be a function that takes some text to be searched and an array that we want the results pushed into so it's taking all the parameters after the the first original parameter so that's what i tried to do so i i thought this would be a really good demonstration of the types that we've just looked at so you see here we've got a type called partially invoked using a type called any func which just represents any possible function so what we're going to do here is we're going to say okay so whenever we try and call this it's going to return instead a function that takes the first parameter and that's going to return a function that takes the remaining parameters using the types that we've just discussed okay so i gave that a go and what i didn't realize or i hadn't thought about was that other params there it's actually going to return the the remaining parameters in a tuple so i needed some way to erase the tuple so here's a standard javascript solution using the types developed in the previous slide and it doesn't quite work okay so if i actually try it out with this example code here uh it comes close yeah but if you look the uh the remaining parameters need to get put inside a tuple and uh i played with that for close to a day thinking oh maybe i can use the spread operator or there must be some way of removing these parameters but it turns out that can't be done yeah so uh you know in typescript at the minute there doesn't seem to be a way to do that and that made me slightly unhappy yeah so i went out into the garden and thumped the tree for a while and then came back and had a good night's sleep and had to think about it and then came up with uh a solution yeah so um here's the the same solution as before except now you see we're using this uh remainer type here so uh we've got this type called uh remainer and this is how it's implemented and that of course is completely obvious and we can just sum up no no absolutely not yeah so this is a little bit weird okay so what we're doing here is that we're using our conditional types but also we're using the unfair keyword so we're saying to type script please and fair for me a that's the type of all the arguments and please and fair for me are that's the return type and then what we're saying is okay if the type of all the arguments can be considered as a tuple of two types we'll please and fair those two types but all i want back is the signature of a function that takes the second type and the return value or if that's not the case yeah so if the arguments can be considered as a tuple of three types you know call it p1 p2 and p3 and by the way and fair those for me well then i want you to disregard p1 and return a function declaration which takes a p2 and a p3 and returns an r or if that's not the case and the arguments can be considered as a tuple of four types well then you get the idea you know so uh we can go out and extend this as necessary yeah or potentially yeah uh we could do it using recursion but at that point that's where my brain blows up yeah so uh richard i'll talk about that in a second yeah but the good news is i was able to go back to my original demo and get this working just fine so uh that made me a very very happy camper so i i retired from the field of honor with a smile on my face yeah however we're just getting warmed up yeah so uh we've seen why conditionals are really useful and we've seen just there with the arguments type why iteration you know uh might be useful but you can't do a while loop at compile time uh but you can however use recursion yeah and uh that's where i hand back to richard yep so as garcia's saying there we now have seen that we can create new types from existing types we can use conditional operators and because of that we although we don't have any sort of procedural loops we can use our recursion so yeah we're going to use recursion to iterate at compile time that's lots of scary words put in the same sentence but let's go and show how that's done okay so to start with we're going to just start looking at recursive types using numbers so in this case what we're going to use is we've got a type called our ink table and the properties you can see both the properties and the values are literal numbers so the property is the value that is pointed to by the property is always an increment of the key so zero points to one one to two and what we can do with that is we can create this type called inc that takes any of those and it takes a number any num any type the extends number now once it does that it checks is that number a key that's inside inktable i.e one of the properties from zero to nine and if it is it looks up that property and returns the value that's set there so um what would happen if we uh if we make an ink of six well that's gonna return 7. and then we can do the same again with when we're going to decrement so we've got a decrement table this time the properties point to one less that a value that is one less so whenever we do a decrement of five we're going to return four so what are we going to be able to do is these well we're going to be able to add in types so how do we do that yeah it sounds a bit crazy so what we're going to do is we're going to take we're going to make a type that's called add that takes two numbers and what you can see there is that it creates an object type with properties again and return whenever it hits return it returns the type a um whenever it hits re add it calls itself again but with a different value and that's where a recursion's happening now if you see it just at the bottom line there we've got this thing b extend zero we're going to hit return so that's our escape clause for the recurs or the recursion loop and then otherwise we're going to hit again and call ourselves a recursive recursively so lots to get in let's walk through an example so say we have we're going to add five and three well that's going to increment five to make six decrement three to two increments six to seven decrement one to zero and then we're gonna have uh come back actually sorry the last line's in but an error but we're going to come back with 8. so we're at that stage when we decrement one we're going to b will be zero we're going to have our escape and hey presto we've added in types lots of fun lots of acrobatics not very useful we'll talk about that in a minute though okay so if anyone's ever worked with single linked lists in value type value space or runtime program in the in any language they'll know that a singly linked list is a list that's made up with an element and then a pointer to another list and so we can actually do the same sort of idea with list types in in the type space now when we say that we're not going to we we mean a specific type of list and that's this triple tape here that we have so a list where we have a defined set of types so in this case we have a sample list and we can see that it it will always be a length three it will always have a boolean is the first value number is the second string is a third and with this with if when we're working with these sort of triple types we can actually recurse over these so to do that just like in value programming with singly linked lists we're going to need to get the head and then get the tail or the rest so first we define functions to do that so you can see here we're taking a head of any list a t that extends a list and with that then we say t extends and then something similar to what garth was explaining earlier a list and we sort of extract out that t has to have at least uh one element so the spread operator on edit any there could be just an empty list but there has to be something there at the head if it does then we're going to return the first element which would be our head element in the case of sample list a boolean otherwise we're returning never which it means it's a type that can never be instantiated and then on rest we again we're going to take our list we do a spread operator on that list we put it in as a parameter and a function this is a lot of acrobatics but if you can see then what we're doing is we're taking we have this underscore annie which means we don't care about the tail uh we don't care about the head part uh we're grabbing and unfairing the tail and then we're at that where it's inferred to type tt and then we return that now again we have our conditional there so we're saying if it doesn't if it's not if it's not able to be done then we're going to return an empty list moving on more so we're just building this up to see what we can do so once we were able to get our head and our tail the other thing we want to be able to get here is the length of a list and you can see here we can actually do that very simply if you do a dot length on a on any type we will get the return type back so if we do that on our sample list we will actually get the type 3 back whereas if we do it on say a standard list where we don't know how many it's in we'll get a type number back so but we can actually build this ourselves using recursion and you can see again we're using the same structure we've got an object with property again a property return we could call them whatever we want that's what we choose for this the return is our escape value where we get our r and then we have a conditional as well so we're saying that once whenever t extends is an empty list then we're going to return that so what are we doing whenever we if t does have something in it what we do is we call again we we get the we get the tail of t which will be number and string and then we increment r which is going to be one we go through this continually until we get our until we've taken everything out of the list and we have the kind that gives us our like okay keep it and we're hopefully hanging in there with this next how do we prepend we can actually pre-pen to lists list types so if we had that that um say we wanted to add create a new type from that sample type in our last slide say with a boolean on it so say boolean string number boolean we can use this function as well again we're just taking our the we're taking the type e which is the the type we're going to put on the at the start of the list and the list that we're going to prepend it to to create this again doing a bit of acrobatics we go and extract with spread operators e and t uh infer a new type u and then are able to return that and then from this what we can do is reverse a list and this is where we're finishing with these lists so what we can do is take this sample list boolean number and string reverse it and come out with a new type of string number and billion again using a recursion working our way through it we've actually what we're doing is we have two lists the one that comes in uh this you know sample list in this case and then one that's continually built up that r that's going to be returned and as we do that we need to sort of uh have our escape value where we say once we've actually gone through every element in t so we're to do that we're just incrementing a number here and so with incrementing prepending and a bit of recursion we're able to actually create something that reverses and how does that work well if you can see we've got our sample list here whenever we actually call reverse on that uh you see the type there is the on data four is actually a string a number and a boolean which is the reverse of the sample list type uh also the length by recursion is coming out with three we see that we've got a tail that's able to be assigned as well and we've got our headers as well so that sounds all a bit weird and wonderful but where would we actually want to use that well actually it is a very useful uh tool to have so if you look at the read-only that we were talking about earlier the read-only type which is actually a type in the typescript library you may find that you want to make more than just the top level properties read only maybe you want to make it deeply nested well you can if you can use a nested recursive deep read-only type to make everything inside that object read-only also if you want to see a little bit more we have actually prepared a little demo a code along that you can go and try where you make an http http get client that is able to catch things like illegal uris that don't exist and all sorts of stuff where we're using map types where we're using these recursive types and it takes you step by step through from just a very simple sort of get function to something that has a lot more complex types it's in the code that we're going to put on we'll just send the link out later and you can follow the video as well back to you guys thank you very much so yeah so as richard was saying that's a really good demo for bringing everything together and it's available online right now so in your copious free time love that phrase it's it's well worth having a we look at so just to draw some conclusions and finish off we've been coding in typespace so i'm sure everybody you know viewing this presentation will be an expert in coding in value space it's the same concepts but we've just taken it to uh to type space instead and uh we very deliberately called this presentation you know type driven development because the new tdd because there's a really nice metaphor there with test driven development because in test driven development well at the beginning people said you had tests you know when tests are a binary thing they say you're right or wrong you know they they determine whether or not you can go on and that's it so you can think about the compiler the same way you know uh with java or c you declare your types and then the compiler either says you're right or you're wrong and that's it yeah but then tdd was really the realization that the test could guide your implementation you know the tests were there to be your friend so the tests couldn't do everything for you but they could help lead you incrementally step by step towards a correct implementation so whenever you're doing type level programming you know whenever you're programming in type space it's the same idea you know we can come up with these types and these types can incrementally lead us towards a correct implementation so we can if you create what the haskell folks call like type holes you know we can say i'm not sure about that yeah and then we can let the the compiler guide us towards a correct solution so it's a very very powerful approach uh there's a url there to a bitbucket repository uh which contains all the examples that you've seen uh in this slide deck and uh there's the link to the the coding demo there that richard did so uh that's our show so
Info
Channel: NIDevConf
Views: 411
Rating: 4.6923075 out of 5
Keywords:
Id: YidUm-CO9kk
Channel Id: undefined
Length: 37min 7sec (2227 seconds)
Published: Fri Jan 01 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.