BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations TypeScript for Enterprise Developers

TypeScript for Enterprise Developers

Bookmarks
51:00

Summary

Jessica Kerr talks about some of the great things in TypeScript, like the flexible type systems and the possibility to test before compilation, but also things that make TypeScript painful. She shows how Node and npm work differently from the JVM (or CLR), and some of the surprises she hit learning this language.

Bio

Jessica Kerr develops development automation at Atomist. After a dozen years in Java, she branched out to Scala and Clojure and Ruby and Elm and more. Nowadays she works in TypeScript, on tools to help developers automate more of our own work.

About the conference

Software is changing the world. QCon empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.

Transcript

This talk is about TypeScript for the Enterprise. In particular, it's about TypeScript for Java, C Sharp, Scala developers. It's about TypeScript on the server, so running on node. If you're a front-end developer, half of this talk will apply to you. If you're a JavaScript Node developer, half of this talk will apply to you. If you are a Java, C Sharp, or, like me, Scala developer, I think you'll get a ton out of this talk and hopefully you won't fear Node quite as much as I should have.

Today what I want to give you is five awesome things about TypeScript. This is not even close to comprehensive. These are not the most awesome things about TypeScript, they're just five of my personal favorites. And then I'm going to tell you five things that tripped me up and hopefully will not trip you up, because you'll expect it until you won't be as angry as I was. This will be fine.

But first, what is TypeScript anyway? So TypeScript is a language, but really it's a compiler, the compiler's executable as TSC. The TypeScript language is a strict superset of JavaScript. You can run your JavaScript files through the TypeScript compiler, or you can rename them to .TS, and they're officially TypeScript and you can run into the TypeScript compiler. So basically the compiler is a function from JavaScript, plus optionally some types as many as you want to JavaScript, again, more JavaScript, different JavaScript, and some type errors. You get to choose how many type errors, also some type definitions, we'll see that soon.

Five Awesome Things about TypeScript

But the interesting part to me is how you get to decide how many type errors you want out of the compiler at the given time. That is my number one favorite thing about TypeScript, the gradual typing. So let me show you how that works. Here I have a Node project, so I've run npm init and I've got a package.json. But I have one empty TypeScript file, but I haven't set this up for TypeScript yet. The first thing you do when you want to run TypeScripts is having installed TypeScript, I run tsc --init. And that creates for me a tsconfig.json file, there it is. See, the thing about the TypeScript compiler is it's very flexible. There are so many compiler options you would never pass them all in on the command line, like you might do with Java C. With TypeScript, you get this whole file, and these aren't all of them, but these are most of them. It's Json plus comments, which is nice of them, because you need them with that many options.

The first ones of interest are the Strict Type-Checking Options. Step one, when I start up TypeScripts, is change strict to flase, step two is change it to false. I'm pretty consistent in that really. So strict to false. And now it's just going to be fine if I give it a JavaScript, it'll be good with that. I'm going to write some JavaScript in this file. Here is a function, printStatus, it takes one parameter which is status and it just does a console.log, that's JavaScript for System.out.println.

Now I'm going to cheat, and this is TypeScript, this interpolated string, that's okay. Then I'll call the function, printStatus running, yes, okay. Then I can test this, I can run ts-node status.ts, which is going to compile it and run it at the same time. Status is running. Can people see this? Is that big enough? So good thing there are 18 screens in this room. But also if I've got the compiler running in the background then I will get a JavaScript file, and I can Node status.js. Let's go look at that JavaScript file. Status.js, it looks just like the TypeScript except that the interpolated string is replaced by string concatenation.

One beauty of the TypeScript compiler is it outputs readable JavaScript. It aims for your JavaScript to look like if you have written JavaScript. JavaScript, let's face it, it's the bytecode of the web currently. Until WebAssembly is sufficiently implemented that we can start targeting that is bytecode, JavaScript is bytecode because it runs in the most universally available runtime, the browser. There are lots of levels of that. Sometimes you want your JavaScript to be tiny if you're shipping it to the front-end, but when you're running it on node, we don't have that problem. We have lots of space on the file system. So TypeScript totally aims for the most readable JavaScript possible, which makes me very happy.

Well, this is pretty identical now because I don't have any types in it, but I want to see what types it inferred. So I'm like, "TypeScript, tell me about your types." This is the next thing I change into tsconfig, I want it to output the declaration files. If you're publishing a library, you absolutely must do this. If it's just an executable, you could get by without it, but it's useful anyway. When I tell it to produce declaration, and notice that I could also ask for source map and more mappy things then I get, oh come on do it, TypeScript, I get a status.d.ts.

This is the types, there's no code in this file, they're only types. The other file has all the code. JavaScript has all the code, the d.ts has all the types, so I can see that it inferred that my parameter has a type of any and my function returns void. So any is the default type in TypeScript, it means - I don't know, it means I have no idea what this is, this is something out of JavaScript plan, I'm going to let you call anything on this thing, and I'm going to let you put anything in it. It's your job. Any is like stepping outside of the type system and you can do that at any time in TypeScript, which is awesome. But I don't want to do it often.

In TypeScript there are kind of like two spaces that the compiler thinks about your code. One is the runtime land, also called object space, and it uses that to produce the JavaScript, this is all the stuff that's available at runtime. And the other space is type land where it's thinking about the types. And it keeps those two things separate and outputs them differently.

Now let's make it pickier. Let's get some meaningful types in here. The first thing I don't like is that my parameter- I didn't even bother to declare or type on it, oh that's so unsafe. I'll go to my tsconfig.json. Before I fix it, I want to make the compiler hate me. Because that way it will stay fixed, and everyone else who starts adding code to this will have to type their parameters, too. This strict option is basically like it's taking all of these six and putting them together. If you set strict true, you get all of these. But if you set strict to false, and you get to pick and choose. So now I'm choosing to set no implicit any to true, and that's going to get me a type error here, and it's going to be angry. Parameter status implicitly has any type. Great, it's yelling at me. This is super easy to fix.

Now it explicitly has an any type and the compiler is happy. Nothing has changed in the types and nothing has changed in the Json, but I'm being explicit, that's nice at me. Next, I want to enforce that I don't use any as a type. Now this gets into one of the really cool things about gradual typing in TypeScript, is it's not all in the compile step. The type errors, you can divide them between the typing step and the linting step because the compiler is also a library and tslint uses the compiler as a library. So tslint is drastically more powerful than jslint because it has both that runtime space and that type land information to use to think about and do static analysis on your code.

So I need to bring in tslint for this one. So tslint --init and, of course, I get another JSON file, tslint.jason in which it tells me, it's going to start with the recommended rules. What are the recommended rules? Good question. I had a hard time finding them. I actually had to look at the library code in order to find the recommended rules, which is a pain, but that's okay. We'll find them as we go along. Well, we'll learn them as we go. Let me make sure I have some sort of error in here.

Let's run tslint. To do things right flags for tslint are -- project. Which says, “Look here for my tsconfig file and actually use the ts compiler for your analysis”. And then format verbose, tell me which rule I just broke. It tells me no console, calls to console.log are not allowed. Come on, care about something else! No, that's all it cares about. Actually that's not the error I was looking for, because I totally want console.log in this program. I mean, it's a silly little tiny demo program, but it wants to console.log.

Now, the awesome thing about tslint, when I put my errors in the linting phase, I get to scope them. Because I can turn off tslint. By default it's going to work everywhere. But I can say tslint:disable, in this case, I'll do next line. I can disable linting at the file level, at part of the file, at the individual line. I can disable linting in general or I can be specific about which rules I want to disable. Am I modifying the JavaScript? That's my problem. There I go again, online JavaScript. Let's put that in a TypeScript. And then I'm going to show you that I can get some errors there.

Now it shouldn't care about console and is going to care about, "Oh my god, there is a missing semicolon. Oh my god, you didn't end the file with a newline which just drives me bonko," except you can say -- fix and then it can add the semicolon so they can add the newline and not bother me, and then we're okay. And now it's all happy, that's good, because we got the comment in for the console.log. I'll warn you, there are some linting with auto fixes that will semantically change the meaning of your code, thank you JavaScript and truthiness. But watch out for those, disable them when you find them.

In the meantime, I am happy that I've disabled no console, and I love that I can scope with this. For instance, I'm going to make it invalid for it to say to type with any explicitly. But now, once in a while, I really do want to use the any type, I want to step outside type safety. I'll be able to override that because it's a lint rule, not the compiler rule, and the lint rules are skippable. The other way to change this, because that comment annoys me, is to go to tslint.json and override the recommended rules. We can say no console, conveniently VS Code here is giving me autocomplete, so I don't care about no console, but I do care about no any. So I'm going to add no any true, and then it's going to be happy here but it's going to give me an error when I run the linter. Come here, run the linter. And it's going to say, "Yes, type declaration of any loses type safety."

Now I can change that. Now, it's going to tell me that and I can make it a string. And everyone is happy. The linter is happy, the compiler is happy. My declaration change to say it's a string, and we're good. Now notice that I can't pass nothing here. The compiler tells me expected one arguments got zero, but I can't pass undefined. Then if I run that ts-node status.ts, it's going to say status is undefined and that's kind of sad. If I want to enforce that you can't just pass undefined or no for absolutely anything, then I'll comment out this strictNullChecks and turn that on, and now there's anger here.

Undefined is no longer a string, I can tighten that. Now, if I do want to accept undefined, I can do that two ways. I can say string or undefined, in which case, I'm allowed to pass undefined, but I'm not allowed to pass nothing. Or I can say status is optional, and then I can pass undefined, I can pass nothing, it's an optional parameter. But right now I don't want it to be optional, I want it to be running. Now I want to tighten my type safety some more. I'll show you the number two of my favorite features of TypeScript, this one is really weird.

I can say, you can't just pass any string, you have to pass literally running. Literal strings are types, they exist in type space as a specific thing. Why? Why would you do that? Well, historical reasons, of course, like everything, TypeScript not only does it have to support all of JavaScript since the beginning of time, it has to support typing the major JavaScript libraries, which is saying something. We have to be able to put useful types on GQuery.

Or, for another example, let's look at a node. One of the built-in modules in Node is child process, and child process, it's what you use to fork, to spawn off another process in the operating system. It gives you back a thing that has this on method on it, and that on method takes as its first argument the name of an event that you're going to trigger on, and its second argument of function to respond to that event. Well, depending on which event, which is a literal string that you pass in, you need to pass a different type of function. If you want to trigger on close then you need to pass a function that takes a number and a string. But if you trigger on disconnect, you better not take any arguments at all because you're not getting any.

One of the most important things about typing for me is when I'm passing functions around. If you're going to do functional programming, you want to know that your functions fit together, that is incredibly hard to debug, hello closure. I really appreciate typing of functions in particular. But in order to distinguish that they had to give me this information so I can use this library correctly. They had to make the literal string a type so that you can overload the method declaration. You can't overload methods, because that's not a thing in JavaScript at runtime. Your types can never affect runtime because they don't exist there. But you can overload the declarations to make sure people get the types right. So that's what they've done here, and it's wonderful and it has the result of literal strings being a type.

Now, it'll catch typos for me. I can't pass running space in here anymore, but that's not super useful if I always pass the same thing. But how about if I can pass running or stopped? Now that starts to make sense, my function is really saying what's meaningful to it. Now, yes, TypeScript has enums. Yes, I should use them here. But I love this because it's cute. It's a great place to start too. As I'm prototyping stuff, I'm just going to set a string in there. I'm not going to go make a separate enum class, blah, blah. I'll come back and make an enum class, that's fine.

In the meantime, let's do something a little more interesting with that. Let's start by making this a type alias. So type status, we’ll give it a name, it's running or stopped. Then let's make these richer. Instead of string types, let's use literal object types. We'll have a kind of status, the kind it might be running, and if it's running, we might know where it's running, so like a host name. Or if it's stopped then we'll give it a kind of stopped. And maybe we have since when was it stopped, that could be a time stamp, we'll call it a number.

Now we have a more meaningful or, it's either a running status or it's a stop status. But then we want to do something with this, now we're going to want to print status.kind, and where we're running, status.where. But that's angry, because TypeScript knows, it knows this is a status, but it might be a stopped status, and that is not going to have the where. So these union types, these or things is totally number three, one of my favorite things about TypeScript. But they're not terribly useful if you can't access those extra fields that it might have, but watch this.

First we'll make this, so this is going to be a kind of running. Come on, make this compile, kind of running where QCon. So this should work, in fact, if I just run it with node, it probably will work. Status is running at QCon. Notice that the compiler compiled it, it's still got type errors, it's angry at us wherever you're on the right, but it compiled it anyway. Because runtime space and type space are so different that their compiler will happily spit out JavaScript even though it has type error. So it can give you the JavaScript and the type errors at the same time. Amazing, it won't output the type definitions, I don't think, when it has compilers.

I really only want it to print out the where if the status is running, and I need to check status.kind here, there we go. So if status.kind is running, and look at that, now it's happy. It knows that if in that condition of my if statement, if I have checked this and that the kind is running, that it definitely has a where, it can deduce that. This is number four, and this is super awesome. Type narrowing within blocks of code. It knows there's a when there, and if it's stopped since when, it's not going to accept that just generally. But if I stick it in an else, then it can do as well. I see that the kind was not running so it must be stopped, and if it stopped then I have a when. The compiler is super, super smart about narrowing these types. And that's fantastic, that actually makes it useful. Now I can ts-node the thing because it compiles, and it's still running at QCon, that's good.

Check this out, if I make status optional, no, you can't even check the kind, because it might be undefined. No, no, no, no. We have strict no checking turned on. But if status is undefined, return, and suddenly it's like "Oh, okay, I see that you're going to return if it's undefined, so that's fine." The rest of the function, it's definitely defined. So that's super amazing. I love that.

Now this, though, it kind of requires a little knowledge into this type thing. What if I'm an OO Developer, in Java and C Sharp and I like to write classes, then I would probably make an interface here. We're going to have a kind of running or stopped. And that's the only common field, so we'll just declare that, and then maybe we'll have a class running status. I do find that TypeScript is immensely more comfortable than JavaScript, especially JavaScript back when I tried to use JavaScript, which was a little while ago. Being able to type things, being able to make classes and interfaces so nice.

I can say we have a kind of running and I can make a constructor. We're going to have a public field, it's read only and its name is where and it's a string anti-constructor body. You can use classes and interfaces instead of type aliases, which is very comfortable. Now, we'll have a new running status of QCon and it'll work in the JavaScript, but TypeScript is angry again, because now status.kind equals running is not enough to say that there's a where field. There could be other implementations of the interface that also have a kind of running.

I have to get more specific in my condition. I can say if status instance of, there it is, RunningStatus, and then it works. I'm not going to make that compiles, so I'm just going to delete that. Then it works, and that's okay except, I do not like exposing concrete classes outside of my conceptual library. Pretend the printStatus is in its own file and shouldn't have to know about the concrete instance of RunningStatus.

What I really want to say is, if this is a RunningStatus then I should have access to where. So here is another really cool thing. In the place where I do know about the implementation of the class, I can define a function is RunningStatus, it takes any status and the return type is really, really weird. Status is RunningStatus, it's super weird. By the way, I'm going to put this comment in here. This is a type guard, that's what it's called, you're going to need that phrase in order to google it every time you use it, because I always forget the syntax. But it needs to return a Boolean about whether status is RunningStatus.

Here I happen to know I can do the instance of, or I happen to know that I'm pretty sure it's going to be RunningStatus that has a kind of running, because I know the implementation at this level. Now it works. You can write custom functions to guide the type system in its narrowing of the types within the block. I love that. I mean it's a little bit of boilerplate, but it works out because then in the JavaScript, that function is doing a real runtime check, because the type information is gone at runtime.

Cool, I can do that. But what if there is another thing here. I'm still exposing the concrete implementation here. There's another thing I could do. I could say, "If it's RunningStatus then its status and it has aware field that is a string." Oops, where is my ampersand? This which looks really weird and is not formatting very nicely. This is type intersection. So just like you can or types, you can and types. TypeScript happily puts them together, because this is all duck typing with one exception that I'll probably rant about later. This all duck typing, so we can just combine types and you can subtract types, and you can run functions over the keys in this type. And you can do really weird things, but I am not going to talk about that today, because I don't think you'll ever use it.

But if you love Scala, and you love to make impossible things not compile, TypeScript is fantastic. If you love using those libraries without actually doing the dance to prove to the compiler that it's going to compile, then you'll love TypeScript. It's a win, you get both. That's number four of what I like, is the type narrowing, fabulous.

Number five. We have to broaden, we have to get into the ecosystem as a whole, because we don't program in languages, we program in language systems including the dependency manager, including the build systems, including the linters, including the runtimes. The syntax of language is a piece of the surface, and JavaScript is a very rich ecosystem. It's very nuanced and deep, and there's a lot of stuff in it, which is one reason to use it. But it's also one reason that it's challenging, especially when I came at this from Java and having been in Java for a dozen years, so not remembering how painful it was to learn this stuff. I was like what even is going on here?

I had to learn some history of JavaScript for this to make sense. And it turned out there are so many JavaScripts. On this timeline here I've charted only the specs. ECMAScript is the spec, which various JavaScripts implement some of, and various portions of. Even then you've got every browser implements, what the heck it does, and it's just super subtle. TypeScript wants to support all of this. But the result of all these varying definitions of JavaScript means that you almost never write JavaScript in the same version that you want to run. Because if you're a front-end developer- can you tell I'm not a front-end developer? Because I don't know all these built tools and stuff, but I know I've heard of them and I'm trying to avoid them for as long as I can.

It's a little simpler on the backend, because you write in one version, some modern version of JavaScript that you select. And then you run it through Babel or whatever to get the older version of JavaScript that will run on all of the browsers that you're targeting. It's easier on the server, fortunately, and it's easier with TypeScript, because I don't need to run in all the browsers. I need to run in whatever version of Node I'm running. Or if it's a library, whatever version of Node I want to acquire the library's users to run.

I can choose which version of the standard I'm going to target. And I don't need any of this stuff. I'll just use TypeScript compiler. And tslint, I want tslint in there. I don't have to choose a version of ECMAScript, I get with TypeScript something more advanced than the latest version of ECMAScript. TypeScript keeps up with ECMAScript, it's implementing all the new things as soon as they're pretty solid, that they're going to be in the standard, TypeScript is right there. So you get to be way ahead and also choose your runtime JavaScript. That's one of my favorite things, and it's also my number one least favorite thing. I mean, a thing that tripped me up.

Five Things That Tripped Me up about TypeScript

If we look at tsconfig, the first object option up here and basic options is target. Now I know what that means. It means the JavaScript that I expect to execute on whatever runtime, I'm going to run it in. And it's set to es5. You may have noticed but probably not because I moved this pretty fast, that in our JavaScript output we don't have a proper class. We have something that's commented as the class, but it's actually a function, whereas modern JavaScript does have classes and the reason for that is we're set to es5 as a target, which is pretty old. If I set that to something current like es2017, then I have a class.

TypeScript is going to make the most readable JavaScript it can for the target you've selected. This is a good idea, but it's also confusing. It gets a lot more entertaining when you switch it, when you have an async function. If I make this function async, this is another of my favorite things but it didn't make the list, then I'm still targeting es2017, so it's going to be fine. It's going to be like, async function, no problem, I got that. But if I set this back to es5, where is the JavaScript? Actually this is TypeScript being very kind, defining some constants that you don't have to read in order that your code be as readable as possible, because the printStatus function looks a lot like my printStatus function. You also get source maps, but in the meantime, the JavaScript actually looks like it. Now, your stack trace is not going to be pretty.

So that's why you should target the most advanced version of JavaScript that you can get away with for the version of Node that you're going to run, and that's super helpful. But there's another problem with this, so let's get into evil thing number two, and hopefully you won't be surprised by this.

PrintStatus is angry. It's angry because it requires the promise constructor. Why? If we go look at the type declarations, because I made it async, it's returning a promise of void. Let's just declare that explicitly, so that it will be angry over here for me. It's angry because it doesn't have the promise constructor because that's not in es5, the standard. But I can get more explicit, because runtimes don't just implement one version of the spec or another, that would be too easy.

TypeScript lets you get more specific and it's got this lib thing. First of all, lib, entirely type space. There is nothing runtime about this. You want your lib - whatever they call it, they call it lib - you want your lib declaration things to match what's available in your runtime, unless you're importing libraries that require stuff, and then you want them to match whatever makes your libraries compile. I just start adding stuff until it gets happy, and then hope I don't hit that in runtime. But I have yet to have a problem with it so this is probably fine.

Anyway, it wants the promise bit so I can tell it, yes. In node, the version of Node that I'm running, which is true, I am going to have the es2015.promise. So then the promise can get happy except, look at this, look at this garbage. TSC - oh no, what's a number? What's an object? I can't find an array. It took me till a couple days ago. I finally figured out what's happening here. What’s happening here is that when I override, where did my tsconfig go, when I override the lib at all, it takes away the default. And the default was es5. Because I'm targeting es5, you’d think that would be available. Well, if you add anything to the lib then you have to say, "Oh, yes, and the obvious thing." Now it's going to be happy and it's going to compile, and it'll be okay and it will target es5.

I forgot to tell you how you know which ECMAScript to use. There is a wonderful website called node.green, whatever, it's great. It's two words, node.green. Up here it says es2015 support and across here you have the various versions of node, and how well they support es2015, nobody does, whatever. If you want to know what Node or whether you can use es2017, you scroll, there's a lot in es2015. Okay, we made it to es2017, and it's telling us that as long as you're running Node 9, it's 100% complete, you're fine. If you want 2018, scroll until this says 2018 up here, then you need to be a Node 10.

So that's how you know, and this is divided up into all those like different feature things. As you get more granular with your lib, you can check. But it only matters if you actually hit that code at runtime anyway. Just finally in TypeScript 3.1, they've made a way for libraries to declare which of these things they need, which is totally helpful because I'm tired of guessing. That is totally number two. You have to like specify your lib sometimes.

Number three is right in between. Give me your tsconfig. Oh, there is. You also get to specify what module system you're using. What? I mean, coming from Java that makes no sense. I use packages, I put them on Maven, but no. JavaScript, way back, of course, this is a historical reason, didn't have a modular system at all. Everything is in the global space. You want privacy? The only way to get privacy is within a function. So people start making functions and then immediately calling them which confuses everyone who was not them or a JavaScript developer, but that's okay. Then people come along and they're like, "If we standardize the way we do these functions that we immediately call, then we can share them." So they start inventing module systems, plural.

If we go back here, I mean es6 actually includes es6 modules as the spec, which Node doesn't even implement, except in experimental mode. That would be too easy, but meanwhile, before that, there's all these others. Someone made CommonJS, which is consistent with required.js. But this is for this server, I think it's so confusing. Then there's module system AMD, which is optimized for the front-end where you actually care about how many bytes you're shipping over the wire, which is not a concern on the server. And then there are some other ones that, fortunately, if you're on the server, you're on node, you pick CommonJS, you're done. But you can output more, and TypeScript, will happily make your code compatible with as many modules systems as you choose. But that totally confuses me.

Also, another thing to know about modules. Now a module is a file, is a module, is a file, except when the file is a script, and ours is a script. How do you know it's a script? Well, it doesn't participate in the module system. It doesn't export anything and it doesn't import anything. And that means when the JavaScript is out, it's just out. It's just doing its thing, it didn't put anything weird. Well, okay, sorry, async maybe should make that not async, then things would get prettier. But it's just JavaScript. If you no longer promise, bam. Oh, no, it hates me, quick undo. We're just there.

Now, it's not a module, it's just a script. But, if I choose to participate in the module system by exporting or importing - so let's export the printStatus function - then my JavaScript looks different. I got this use strict thing, we got this defineProperty. I don't really know what that is. There are some exports down here. But because I'm using TypeScript, TypeScript knows what that is, it knows how to work with the module systems, and I don't have to deal with it.

Modules, number three. Then there's a way that you bring in the modules in the import statement, and that's also interesting. I can export stuff, and that's going to be available to other files. They can import it from my module as a file, as a module. But, if I want to bring something in, say I want to bring in that child process module, that's built into node. There are two ways to do that, import star as CP or whatever alias from child process, great. Now, I can do cp.spawn and stuff like that, fabulous. Or I could pick just the pieces that I want, and I could import spawn from child process. This is actually doing object deconstruction there, which is another cool thing that didn't make the cut.

So we're pulling out just the spawn property as its own identifier, great. Those are straightforward because child process exports an object with a bunch of functions and constants on it, like a good module should in my very strong but not entirely shared opinion. But then, not every module works like that - that would be too easy. Let's bring in a new package. This is always a challenge. When I print this status, I want it to be in a box. I want to use this package boxen, which I can get from npm. You can get everything on npm in triplicate, but you may not like any of them, that's okay. It's there or whatever it is. This boxen is beautiful. It does little ASCII art boxes. I really like it.

To get it, I need to run npm install boxen, and npm is going to declare it in my package.json. It's going to bring in boxen 2.0, it's going to put in my Node modules. Let's look at Node modules here for a minute. Node modules is a directory on my file system inside my project, and here it has boxen. It also has all this other crap. Why does it have all this other crap, npm, ls? This the tree of module dependencies. Look, my tiny module boxen depends on all of these. So here I have all of these. This is just normal. I think in our real projects we have at least 100. Welcome to dependency land, it's fine. Npm is a very important part of the language system of Node and of JavaScript now. Just like Maven is crucial to Java, and you may have heard bad things about npm, but they're getting less and less true. Npm now as a company behind it, it's totally improving. It's come a long way since that left pad incident, which I totally love; when someone deleted one tiny library with one function in it, and the whole internet went down, because heaven forbid we duplicate a function that's this long. No.

Anyway, npm has come a long way since then but it's still rich. The ecosystem is rich. One nice thing about having all the code on your file system is you can come look at the JavaScript, it just brings the code right in. JavaScript is the bytecode and it's readable, unlike Java bytecode, so that's kind of handy. Sometimes I just go ad some debugging to the JavaScript, write my Node modules. Whenever it goes away, the next time I run npm install, and it gets replaced by the real version, but it's totally great for debugging. I do like that about it. So that's a plus.

But this is a JavaScript module, and it does not export an object. No, it exports a function. Thank you, boxen. So that's fine - I wanted a function, but I can't import it like import star as boxen from boxen. No, I have to know that I need to say import boxen from boxen, and how do you know to do that? Well, you go look at the code or you go look at this page on npm, and you just observe that it uses itself as a function. We’ll come back for that type error in a minute. I think this is actually going to work. Did anybody spot the bug? I haven't run it in a while, which was my mistake, but I did introduce a bug. It says status.kind is undefined. Indeed. Make everything else go away, JB. Status.kind is undefined because look, I gave it a type but I did not give it a value. This is a total trick.

Step one in this is make it yell at me. Sure enough, if I go to my strict options, there's one that says strictPropertyInitialization. Why this is separate from no implicit any, I have no idea. But there it is, and now when I go back to status, it's going to yell at me that I did not initialize kind. I need to say kind is the type of running and it's equal to running. This looks weird, but yes, you have to give it that constant string in type space and in runtime space. It gets into the JavaScript, and into the declarations. Now you know, and when this frustrates you, you will say "Oh, darn, now I feel extra stupid, because she told me about that." Just get your compiler to yell at you, it's better.

But speaking of the compiler yelling you, it's still yelling at me about boxen, because it could not find a declaration file for boxen. Because we said no implicit any, I actually wanted it to care about implicit any in my code. But, no, it's all the code in the world. No implicit any, this is a JavaScript library, people, it's in any. Just deal with it, but that's not explicit enough for it now, because I said no explicit any. And it wants me to go look at type/boxen. So what it needs is a declaration of the boxen module, it just has to have one, it's desperate for one in order to compile. Did I test it again? When I mean in order to compile, I mean compile without errors. TypeScript, because I fixed that problem. And it should, yes, there's the box.

So even though the compiler is still angry at me, I have a box. Can you all see the box? I love the box, I can make it bigger, I can make it yellow. I think boxen is really awesome. Anyway. But what's not awesome is that it hates me, about the boxen. If boxen were TypeScript module, it would have the types built-in, that's one place to get them. It would have the declaration files shipped with the package. But if boxen is not and it's not, then sometimes people put the declarations in this npm at types user, and there's a ton of them there. This is not one of them. So I need to tell it, I'm sorry, but this is just not going to be a thing.

Here is the spell, it's totally a spell. If I make a file, I don't think it matters what its name is, but it matters that it's the top level. And I say declare module boxen. That is the explicit any. If I want no implicit, I need to do things right. I can say declare module star. Let the JavaScript in, it's going to be okay. Trust me, I'll deal with the errors at runtime. Some people prefer that to be explicit, but this is going to make it happy. Now, it's an explicit any, it's compiling, it's out putting on JavaScript. Whatever, we're still in es5, so it's going to do complicated things with the imports to try to make them read well. I don't have to understand it fortunately. Here is our type declarations and there is the function takes a status, great.

Everything is happy now. I wanted to talk a little bit about distinguishing a package from a module, just because this is something that confused me. That's a good thing. A package is something that you publish to npm. It has a default module or main module which defaults to index.ts in the case of a TypeScript project, which becomes index.js and index.d.ts as opposed to a module, which is a file. I get those mixed up that's annoying.

But the other thing that's annoying is how different it is from Java when it loads these packages in these modules, because we're just going to do this abstractly. If this is my project, my TypeScript project, and I depend on boxen, which I do, and boxen depends on chalk, which it does. Chalk is just some ASCII art library thing. Then when npm does the install, it puts this stuff in Node modules, because it's matching what Node does when it loads it, and TypeScript has yet a third implementation of that, where tries to imitate nodes module loading. When you get into this, you will wind up spending half a day, at some point, learning about the intricacies of module loading in node.

At some point, we did this in Java. At some point, I had to learn class paths and class loaders. It's not necessarily worse, but it's very different. So for instance, what npm does is it doesn't put chalk there. It puts chalk up at the top level, because, in addition to looking in Node modules, and Node will look in your own Node modules directory first. If it doesn't find your boxen, it doesn't find chalk there, then it goes up a directory and says, "Hey, do you have a Node module?" It'll look in there and then it’ll go up a directory and "Hey, do you have Node modules?" And it'll go look in there. So boxen winds up finding chalk in my Node modules directory.

This is npm trying to reduce duplication, because what if I also depend directly on chalk, then I would have chalk at the top level. As long as those are compatible versions, that's fine. Boxen and I can share a version of chalk, which is great, unless they're not compatible versions. In which case, boxen gets its own version of chalk. So here we have chalk1.x, maybe and I've got chalk 2.x, I'm making that up.

On one hand, this is not what would happen in Java. In Java, you have a class path, and you're going to find the first one of that class on the class path. And everybody's going to use that one unless you do magic class loader tricks, in which case you deserve what you get. But it's different here. Boxen is actually getting a different version of chalk, which is totally fine. If boxen just uses functions in chalk, it can have the ones it expects, and I can have the ones I expect, and everyone's happy, until boxen starts constructing something from chalk, giving it back to me, and I start passing that into my chalk.

What if that thing is a class? And what if that class has private or protected fields? So TypeScript does duck typing entirely, except in the case of classes with private or protected fields. Because private and protected, not a thing in JavaScript, not a thing in runtime. That has to be enforced entirely at compile time. If chalk exports a class which it doesn't, it's not that route, and then accepts a class into a function but we have had this problem in our libraries before we learned better, and that class is probably a protected fields. The only way TypeScript knows the class that chalk wants is the one being passed in is if it's in the same place on the file system like your personal file system.

You get this problem in Java. If you load a classroom, two different class loaders is the same class, and Java is like … But in node, or in TypeScript specifically, this is not node, this is TypeScript, even if it's the exact same version of the class, maybe you did a link to a local library, and it happens to have the exact same version of the class, but in a different location on the file system, TypeScript says, "Compile error private fields," super confusing. That's okay, because why are you exporting a class, a concrete class anyway with private fields? And why are you asking for that? Interfaces, people, this is just good OO design.

TypeScript is making it even more painful for you to violate the OO design principle of don't export concrete classes in your external API use interfaces. Maybe that's nice of it. But, now you're warned and it's just weird, and the point of the whole thing is really that you're going to get frustrated. When you switch to a new language it's not just the language. That's the easy part. It's a whole ecosystem. At some point, you're going to have to dig in and learn each part of that ecosystem.

When you're frustrated - this is my new lesson for myself this month actually - when I'm frustrated, when I'm cursing, when I’m banging on the computer and telling Node how stupid it is, it's time to slow down. It's time to step back and be like, "Okay, the world is telling you that you don't understand it." The way I think it should be is not the way it is. And in order to make it better, or much more likely to work within it, I'm going to have to step back, slow down and take the time to learn.

So don't be afraid of TypeScript, but also don't be afraid to just stop, and yes, spend half a day learning about how Node loads modules. Yes, it's esoteric and weird, but I promise it will make you a richer person. Thank you.

 

See more presentations with transcripts

 

Recorded at:

Mar 24, 2019

BT