Static analysis is a type of testing that provides automated checking of your code without actually running it or having to write an automated test. You've likely already seen this kind of testing if you use an IDE like VSCode—the type checking performed by TypeScript is a kind of static analysis, and it can show up as squiggly lines under errors or warnings.
ESLint
ESLint is a tool that can provide feedback on possible problems in your codebase. These problems may be typesafe, but errors or nonstandard behavior in their own right. ESLint lets you apply a number of rules that are checked on your codebase, including many in its "recommended" set.
A good example of an ESLint rule is its
no-unsafe-finally
rule. This prevents you from writing statements which modify your program's
control flow inside a finally
block. This is a great rule, because doing this
is an unusual way to write JavaScript that can be hard to follow. However, it's
also something that a healthy code review process should be able to detect.
try {
const result = await complexFetchFromNetwork();
if (!result.ok) {
throw new Error("failed to fetch");
}
} finally {
// warning - this will 'overrule' the previous exception!
return false;
}
As such, ESLint isn't a replacement for a healthy review process (and a style guide that defines what your codebase should look like), because it's not going to capture every unorthodox approach that a developer might try to introduce into your codebase. Google's Eng Practices guide has a short section on "keeping it simple".
ESLint lets you break a rule and annotate code as "allowed". For example, you can allow the previous logic by annotating it as follows:
finally {
// eslint-disable-next-line no-unsafe-finally
return false;
}
If you find yourself constantly breaking a rule, consider turning it off. These tools encourage you to write code in a certain way, but your team might be used to writing code in a different way and already be aware of the risks of that approach.
Finally, enabling static analysis tools on a large codebase might create a lot of unhelpful noise (and busywork to refactor) over code that otherwise worked fine. So it's easier to enable early in a project's lifecycle.
ESLint plugins for browser support
You can add a plugin to ESLint that flags the use of APIs that aren't widely supported, or not supported by your target browser list. The eslint-plugin-compat package can warn you when an API might not be available to your users, so you don't have to constantly keep track for yourself.
Type checking for static analysis
When learning JavaScript, new developers are typically introduced to the idea that it's a weakly typed language. That is, it's possible to declare a variable as one type, then use the same location for something completely different. This is similar to Python and other scripting languages, but unlike compiled languages such as C/C++ and Rust.
This kind of language might be good for getting started—and it's arguably this
simplicity that has made JavaScript so popular—but it's often a point of failure
for some codebases, or at least something that allows confusing errors to
happen. For example, by passing a number
where a string
or an object type
was expected, that incorrectly typed value can propagate through various
libraries before finally causing a confusing TypeError
.
TypeScript
TypeScript is the most mainstream solution to JavaScript's lack of typing information. This course uses it extensively. And while this isn't a course on TypeScript, it can be an important part of your toolbox because it provides static analysis.
For a quick example, this code, which expects to be given a callback accepting a
string
name and number
age:
const callback = (name: string, age: string): void => {
console.info(name, 'is now', age, 'years old!');
};
onBirthday(callback);
Generates the following error when run through TypeScript, or even when hovered over in an IDE:
bad.ts:4:12 - error TS2345: Argument of type '(name: string, age: string) => void' is not assignable to parameter of type '(name: string, age: number) => void'.
Types of parameters 'age' and 'age' are incompatible.
Type 'number' is not assignable to type 'string'.
4 onBirthday(callback);
~~~~~~~~
Found 1 error in bad.ts:4
Ultimately, the goal of using TypeScript is to prevent errors like this—
age should be a number
, not a string
—creeping into your project. This
kind of error can be difficult to detect using other types of test.
Additionally, the type system can give feedback before a test is even written.
This can make the process of writing code easier by giving you early feedback
about type errors as you're developing software, rather than when the code
eventually runs.
The most challenging part of using TypeScript is setting it up correctly. Every
project needs a tsconfig.json
file, which, while primarily used by the tsc
command-line tool itself, is also read by IDEs like VSCode along with many other
build tools and tooling, including Vitest. This file contains hundreds of
options and flags, and you can find some good resources for setting it up here:
General TypeScript tips
When setting up and using TypeScript through a tsconfig.json
file, keep the
following in mind:
- Make sure your source files are actually included and checked. If a file mysteriously "has no errors", that's probably because it's not being checked.
- Explicitly describing types and interfaces inside
.d.ts
files, rather than having them implicitly described as you write functions, can make your codebase easier to test. It's easier to write mocks and 'fake' versions of code when the interfaces involved are clear. .
TypeScript implicit any
One of TypeScript's most powerful and rewarding configuration options is the
noImplicitAny
flag. However, it's also often the most difficult to enable,
especially if you already have a large codebase. (The noImplicitAny
flag is
enabled by default if you're in strict
mode, but not otherwise.)
This flag will make this function return an error:
export function fibonacci(n) {
if (n <= 1) {
return 0;
} else if (n === 2) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
Even though, as a reader, it's fairly clear that n
should be a number,
TypeScript can't confidently confirm this. If you're using VSCode, hovering over
the function will describe it as follows:
function fibonacci(n: any): any
Callers of this function will be able to pass through a value of type any
(a type which allows any other type), not just a number
. By enabling the
noImplicitAny
flag, you can safeguard this kind of code during development,
without needing to write extensive business logic tests for your code passing
the wrong data types in specific places.
The simple fix here is to mark both the n
argument and fibonacci
's return type as number
.
The noImplicitAny
flag doesn't prevent you from explicitly writing any
in
your codebase. You can still write a function that accepts or returns
the any
type. It just ensures that you give every variable a type.