20 TypeScript Compiler Options for your TSCONFIG.JSON

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
These are the 20 TypeScript compiler options that we recommend We grouped them into 4 categories: strict mode, no unused code, no implicit code and others. Switching from JavaScript to TypeScript is already a huge improvement in any codebase, but we can do better. We can go beyond the default TypeScript checks and make our codebase much safer. To do that, we'll need to set some options for the TypeScript compiler. In this video, I'll show you 20 TypeScript compiler options that my team and I use and recommend to our clients. Then, we'll go over each of those flags to understand what they do and why we recommend them. I'm Lucas Paganini, and in this channel, we release web development tutorials. Subscribe if you're interested in that. We also publish our content as articles on our website. You can subscribe to our newsletter to be notified when we launch a course, post a video or write an article. I'll show you the flags in a second, but first, a warning. Sometimes, just knowing what to do is not enough. For example, if you're working in a big codebase, enabling those flags will probably cause a lot of compiler errors. My team and I have been there, most of our clients hire us to work on Angular projects which use TypeScript. So trust me when I say that we understand the challenge of tackling tech debt in big legacy codebases. So besides this video, we're also working on another video that will go over our techniques to deal with the compiler errors that arise from enabling each of those flags in a big legacy codebase. If that's of your interest, check the description. If the next video is done, there will be a link for it. Otherwise, subscribe to the channel to be notified when we release it. On second thought, just subscribe anyways. Without further ado, these are the 20 TypeScript compiler options that we recommend: Of course, a real tsconfig.json would also have other flags, such as target, outDir, sourceMap, etc. But we're only interested in the type checking flags. We grouped them into 4 categories: strict mode, no unused code, no implicit code and others. Beware, as TypeScript evolves, new flags will be created. Maybe there is a new flag that didn't exist by the time we recorded this video, so along with this list, we also recommend that you take a look at the TypeScript documentation to see if they have any other flags that might interest you. Another way of staying updated is to go to lucaspaganini.com and subscribe to our newsletter. Just saying... Let's start by breaking down the strict mode flags. As you can see, there are 9 flags in the strict mode category: The first one, "strict", is actually just an alias. Setting "strict" to true is the same as setting all the other 8 strict mode flags to true. It's just a shortcut. But that's not to say that it doesn't provide value. As I've said before, TypeScript is evolving. If future versions of TypeScript include new flags to the strict category, they will be enabled by default due to this alias. JavaScript also has a strict mode, it was introduced in ES5, a long time ago. To parse a JavaScript file in strict mode, you just need to write "use strict" at the top of your js file, before any other statements. Enabling "alwaysStrict" in the TypeScript compiler ensures that your files are parsed in the JavaScript strict mode and that the transpiled files have "use strict" at the top. The JavaScript strict mode is all about turning mistakes into errors. For example, if you misspell a variable, you're expect an error to happen. But if you're running JavaScript in "sloopy mode", that won't throw an error. Instead, it will set a global variable. So the benefit of enabling "alwaysStrict" in TypeScript is that those runtime errors you'd get from the JavaScript strict mode are turned into compiler errors instead. noImplicitAny is pretty self-explanatory. It won't allow your code to be implicitly inferred as any. So, if you explicitly cast a type as any, that's fine. But if TypeScript implicitly infers that something is any, you'll get a compiler error. The benefit here is that inference issues won't go unnoticed. I'll give you two different examples where this flag would save you: First, let's say you declare a function to split a string by dots. Your function should receive a string and return an Array of string<s>. But you forget to set your parameter type to string, what happens now? The parameter type will be set to any by default and TypeScript won't even warn you. Here you are, thinking that you're leveraging all the power of TypeScript, but your function has no type-safety whatsoever. TypeScript will allow other developers to use your function with whatever types they want. TypeScript will say it's ok for them to pass a number, or a Date, or an airplane. But it doesn't stop there. Let's go to the second example. Even if you do pass a string to your function, instead of an airplane, you can still have issues. Let's say you call your function and save the returned value in a variable. You think that your variable is an Array of string<s>, but it's actually any. If you try to call Array.forEach and misspell it, TypeScript will let you shoot yourself in the foot, because it doesn't know that your variable is an Array. Enabling noImplicitAny would raise a compiler error in your function declaration, forcing you to set the type of your parameter and preventing those inference issues to go unnoticed. And if you really want your parameter to be of type any, you can explicitly type it as any. noImplictThis is similar, it will raise an error if this is implicitly any. The value of `this` in JavaScript is contextual, so if you're not 100% confident with how JavaScript sets this, you might write a class like the following and not notice your error: The issue here is that this, in this.name, does not refer to the class instance. So, calling user.getName()() will raise a runtime error. To prevent that, we can enable noImplictThis, which would raise a compiler error, preventing you from using this if it's inferred to be any by TypeScript. By default, TypeScript ignores null and undefined, you can change that by enabling strictNullChecks. That's by far the most valuable flag of all. It will rightfully force you to deal with null and undefined. Think about this for a second. You're calling Array.find and expecting to receive an element of the array. But what if you couldn't find what you were looking for? It will return undefined. Are you dealing with that? Or will your code break during runtime? We should obviously always deal with null and undefined. I get that you'll have millions of errors when you enable this flag, but if you're just starting a new project, this is a no-brainer. You should definitely have it enabled. strictBindCallApply enforces the correct types for function call, bind and apply. I don't generally use call, bind, or apply, but if I were to use it, I'd like it to be correctly typed. So I don't even know why that's an option, they should be correctly typed by default. strictFunctionTypes causes function parameters to be checked more correctly. I know, that sounds weird, but that's exactly what it does. By default, TypeScript function parameters are bivariant. That means that they are both covariant and contravariant. Explaining variance is a topic on its own, but basically, when you're able to assign a broader type to a more specific one, that's contravariance. When you're able to assign a specific type to a broader one, that's covariance. When you go both ways, that's bivariance. For example, take a look at this code: We're declaring an interface called User that has a name and an interface called Admin that extends the User interface and adds an Array of permissions. Then we declare two variables: one is an Admin and the other is a User. From that code, I'll give you examples of covariance and contravariance. As I've said, when you're able to assign a specific type to a broader one, that's covariance. An easy example would be assigning admin to user. An Admin is more specific than a User. So, assigning Admin to a variable that was expecting a User is an example of covariance. Contravariance would be the opposite, being able to set a broader type to a more specific one. Functions are a good place to find contravariance. For example, let's define a function to get the name of a User and another to get the name of an Admin. getUserName is broader than getAdminName. So, assigning getUserName to getAdminName is an example of contravariance. Again, bivariance is when you have both covariance and contravariance. After that super brief explanation of variance, let's get back to our strictFunctionTypes flag. As I've said, TypeScript function parameters are bivariant. But that's wrong. As we've seen before, most of the time, function parameters should be contravariant, not bivariant. I can't even imagine a good example of bivariance in function parameters. So when I said that `strictFunctionTypes` causes function parameters to be checked more correctly, what I meant is that TypeScript will not treat function parameters as bivariant if you enable this flag. After explaining this flag, you will see that the next ones will be a breeze. strictPropertyInitialization makes sure you initialize all of your class properties in the constructor. For example, if we create a class called User and say that it has a name that is a string, we need to set that name in the constructor. If we can't set it in the constructor, we need to change its type to say that name can be either a string or undefined. The motivation for this flag is the same as the one for strictNullChecks, we need to deal with undefined, not just ignore it and hope for the best. The last strict mode flag is useUnknownInCatchVariables. This flag will implicitly type a variable in a catch clause as unknown, instead of any. This is much safer because using unknown forces us to narrow our type before any operations. Check this one-minute video that I made explaining the differences between any and unknown. The link is also in the description. For example, it's fairly common to expect that our catch variable will be an instance of Error, but that's not always the case. JavaScript allows us to throw anything we want. We might throw a string instead of an Error. That becomes problematic when we try to access properties that only exist in an Error, such as .message TypeScript will let you do whatever you want, because by default, catch variables are typed as any. But if we enable this flag, that code will break, because TypeScript will type catch variables as unknown, forcing us to type-check that our variable is indeed an Error instance before accessing the .message property. Now we'll get into the no unused code flags. There are 4 in this category: As you'll see, those flags are more like linting checks than compilation checks. The first one, noUnusedLocals, will emit an error if there are unused local variables. It won't necessarily point out a bug in your code, but it will point out unnecessary code, which you can remove and reduce your bundle size. noUnusedParameters does the same thing, but for function parameters instead of local variables. It will emit an error if there are unused parameters in functions. The motivation is the same, remove unnecessary code. Tip: if you really want to declare some unused function parameter, you can do so by prefixing it with an underscore _. In our example, we could change age to _age and TypeScript would be happy. JavaScript is a multi-paradigm programming language, it's imperative, functional, and object-oriented. Being an imperative language, it supports labels, which are kinda like checkpoints in your code that you can jump to. Labels are very rarely used in JavaScript, and the syntax to declare a label is very close to the syntax to declare a literal object. So most of the time, developers accidentally declare a label thinking that they're creating a literal object. To prevent that, we can set allowUnusedLabels to false, which will raise compiler errors if we have unused labels. Another thing that we can safely get rid of is unreachable code. If it's never going to be executed, there's no reason to keep it. Code that comes after a return statement, for example, is unreachable. By setting allowUnreachableCode to false, the TypeScript compiler will raise an error if we have unreachable code. The next category is "no implicit code". There are only 2 flags here. By the way, noImplicitAny and noImplicitThis could also come into this category, but they're already in the strict mode category. If you use a lot of object inheritance, first: WHY!?!?. Second: TypeScript 4.3 introduced the override keyword, this keyword is meant to make your member overrides safer. Back to our User and Admin analogy. Let's say that User has a method called greet and you override that method in your Admin class. All good and well, until User decides to rename greet to saySomething. Then your Admin class will be out-of-sync. But fear not, because, with the override keyword, TypeScript will raise a compiler error, letting you know that you're trying to override a method that is not declared in the base User class. Along with the override keyword, we got the noImplicitOverride flag. Which requires all overridden members to use the override keyword. Besides making your overrides safer, this has the added benefit of making them explicit. Talking about explicit, we also have the noImplicitReturns​ flag, which will check all code paths in a function to ensure that they return a value. For example, let's say you have a function getNameByUserID that receives the ID of a user and returns his name. If the ID is 1, we return Bob and if it's 2, we return Will. There's an obvious issue here. What would happen if we requested the name of a user with ID 3? Enabling noImplicitReturns​ would give us a compiler error saying that not all code paths return a value. To deal with that, we have two alternatives: First. Deal with cases where ID is neither 1 nor 2. For example, we could return Joe by default. Two. We can maintain our implementation as it is, and explicitly type our function return as string or void. That goes back to the strictNullChecks motivation. We need to deal with all the possible cases. This flag should be always on. The last category is "others". Basically, every useful flag that I couldn't fit into the previous categories. There are 5 flags here Let's start with noUncheckedIndexedAccess. TypeScript allows us to use index signatures to describe objects which have unknown keys but known values. For example, if you have an object that maps IDs to users, you can use an index signature to describe that. With that in place, we can now access any properties of usersMap and get a User in return. But that's obviously wrong. Not every property in our usersMap will be populated. So usersMap.example should not be of type User, it should be of type User | or undefined. That seems like a perfect job for strictNullChecks, but it's not. To add undefined while accessing index properties, we need the noUncheckedIndexedAccess flag. On the topic of index properties, we also have the noPropertyAccessFromIndexSignature flag, which forces us to use bracket notation to access unknown fields. This is very much a linting flag. The benefit here is consistency. We can create the mental model that accessing properties with dot notation signals a certainty that the property exists, while using bracket notation signals uncertainty. Another very useful flag is noFallthroughCasesInSwitch. If we declare a switch case without break or return statements, our code will run the statements of that case, as well as the statements of any cases following the matching case, until it reaches a break, a return or the end of the switch statement. Even if you're a senior developer, it's just too easy to accidentally forget a break statement. With noFallthroughCasesInSwitch enabled, TypeScript will emit compiler errors for any non-empty switch cases that don't have a break or a return statement. Protecting us from accidental fallthrough case bugs. Another easy mistake is to rely on case-insensitive file names. For example, if your operating system doesn't differentiate lowercase and uppercase characters in file names, you can access a file called `User.ts` by typing it with a capital "U" or with everything lowercase, it'll work the same way. But it might not work for the rest of your team. By default, TypeScript follows the case-sensitivity rules of the file system it’s running on. But we can change that by enabling the forceConsistentCasingInFileNames flag. When this option is set, TypeScript will raise compilation errors if your code tries to access a file without exactly matching the file name. We get consistency and avoid errors with case-sensitive operating systems. The last flag I'd like to mention is exactOptionalPropertyTypes. In JavaScript, if you have an object and try to access a property that doesn't exist in it, you get undefined. That's because that property was not defined. For example, declare an empty object called test and try to see if property exists in test. We'll get false. Ok, we get that. But we can also explicitly define a property as undefined, and that's a little different because now, if we try to see if property exists in test, we'll get true. That's all to say that there's a difference between a property being undefined because it wasn't defined and it being undefined because we set it to undefined. By default, TypeScript ignores that difference, but we can change that behavior by enabling exactOptionalPropertyTypes. With this flag enabled, TypeScript becomes aware of those two different ways of having an undefined property. When we use the optional property operator(?), we indicate that a property might be undefined by not being defined. But that won't allow us to explicitly set that property to undefined. If we want to explicitly define a property as undefined, we'll need to say that it can be undefined. That's all. If you want to dive deeper into TypeScript, I have a series about TypeScript Narrowing. You can watch the full series, for free, on my channel. As always, references are in the description. If you enjoyed the content, you know what to do. And if your company is looking for remote web developers, Please consider contacting me and my team on lucaspaganini.com. Leave a like, have a great day, and I’ll see you soon.
Info
Channel: Lucas Paganini
Views: 17,108
Rating: undefined out of 5
Keywords: web development, typescript, compiler options, typescript flags, noImplicitAny, noImplicitThis, tsconfig.json, tsconfig, typescript config, typescript configuration
Id: I1ZFsPK0Q-Y
Channel Id: undefined
Length: 21min 24sec (1284 seconds)
Published: Mon Jun 06 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.