DEV Community

Barin Britva
Barin Britva

Posted on Edited on

TypeScript: Sorting out tsconfig. Part 2

TypeScript: Sorting out tsconfig. Part 2

In the previous article, I covered a number of quirks in TypeScript’s general settings. This one is about the so-called “strictness flags.”

Out of the box, TypeScript is not very different from JavaScript. Unless you tune the project config from the start, most of the language’s advantages never come into play. There is some point in using TypeScript that way, but not much.

If I had to reduce this entire article to one sentence, it would be this: open tsconfig.json and set strict to true under compilerOptions. Without strict mode, TypeScript is almost a waste of time. Still, neither the previous article nor this one is meant to tell you the one “right” way to do things. The goal is to clarify the finer points of configuration. So let’s get into it.

In the official tsconfig.json reference, its many options are split into sections. Two of them, Strict Checks and Linter Checks, contain only strictness flags. A few more flags relevant today are tucked away in the largest option group: Advanced.

The Strict Checks group

These are probably the most important flags of the lot: strict, alwaysStrict, noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, and strictBindCallApply.

Every flag above defaults to false. To make things strict, set it to the opposite value: true.

Looking at the list, the similarly named strict and alwaysStrict flags can be confusing. In reality, they do completely different jobs.

alwaysStrict

Recommended: always / difficulty: easy.

alwaysStrict adds the "use strict" directive to every compiled file. In other words, it enables JavaScript strict mode. It has nothing to do with TypeScript type checking.

strict

Recommended: always / difficulty: hard / can be replaced by a set of other flags.

strict is directly related to type checking. Enabling it automatically turns on every flag in Strict Checks, including alwaysStrict. That is exactly what I was talking about at the beginning.

This approach has at least one downside: it is opaque. When you set strict: true, you cannot immediately see exactly which checks are enabled or which options exist at all. For projects written in TypeScript from day one, this matters less than it does for projects being migrated gradually from JavaScript.

When migrating an existing application, you cannot switch on every check at once. You have to enable them one by one, step by step. Sometimes problems even force you to roll previously enabled flags back to false.

There is one small detail about strict: its controlled list of flags can grow as new TypeScript versions are released. That does not happen often, and it is always covered in the release notes, assuming that you read them before updating.

Personally, I prefer to list the flags explicitly:

{
  "compilerOptions": {
    "alwaysStrict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "strictBindCallApply": true,
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s look at what each flag does, and discuss which ones to enable first if turning them all on at once is not an option.

noImplicitAny

Recommended: always / difficulty: medium.

This is the first flag you should enable when starting a TypeScript project.

First, a word about any. It is a special type assigned to every variable whose type was not stated explicitly and cannot be inferred automatically by the compiler. It exists for JavaScript compatibility. From TypeScript’s point of view, every JavaScript variable is any, because JavaScript has no type system.

To the compiler, any means: “This can be anything. I am handing control and responsibility to the developer.” Because it does not know the variable’s type, you can do absolutely anything with such a variable. This is where TypeScript loses all of its power:

// type stated explicitly
let a: number = 5
// type inferred
let b = 'hello'
// type is not stated and cannot be inferred
// value will be implicitly declared as any
function someFunction (value) {
  // so the error on this line goes unnoticed
  console.log(value.subtr(3))
}
Enter fullscreen mode Exit fullscreen mode

Put differently, if a variable’s type is unknown, it is any. If it is any, this is JavaScript rather than TypeScript code. So the number of unspecified types in a codebase is a real measure of the ratio between its TypeScript and JavaScript. noImplicitAny highlights every such spot so the developer does not forget to state types.

That does not mean you can no longer use any at all. It only means that in such cases the developer must write any explicitly if, for some reason, the variable cannot be typed correctly, or if typing it causes errors elsewhere in the code.

// explicitly mark code as potentially unsafe with any
function someFunction (value: any) {
  console.log(value.subtr(3))
}
Enter fullscreen mode Exit fullscreen mode

An explicit any reminds the developer that this code is unfinished and that these places need improvement over time. That is the point of noImplicitAny.

You can reinforce this with ESLint’s no-explicit-any rule. It marks uses of any as warnings, drawing a developer’s attention to them every time.

strictNullChecks

Recommended: always / difficulty: medium.

This flag enables one of TypeScript’s most powerful features. I recommend turning it on immediately after noImplicitAny.

JavaScript has two bottom values, undefined and null, and TypeScript has types with the same names. In the type hierarchy, every other type is considered to derive from them. By inheritance, a value of undefined or null can be passed in place of a variable of any type: string, boolean, number, and so on.

function someFunction (value: number) {
  // may produce an unexpected result if value is undefined or null
  return value * 2
}
someFunction(5)
// this is allowed by default
someFunction(null)
// and so is this
someFunction(undefined)
Enter fullscreen mode Exit fullscreen mode

This behavior is true of most, if not all, programming languages. In practice, it can be inconvenient because it requires extra null/undefined checks. The much bigger problem is remembering that your code needs those checks. Java developers, with their NullPointerException, understand this better than anyone.

Enabling strictNullChecks changes the rules of the game. With it on, the compiler no longer lets you pass undefined or null where another type is expected unless you explicitly allow it:

function someFunction (value: number) {
  // value will always be a number
  return value * 2
}
someFunction(5)
// the following function calls are not allowed
someFunction(null)
someFunction(undefined)
Enter fullscreen mode Exit fullscreen mode

That means you do not have to handle undefined and null in code where those values cannot occur. Where they can occur, the compiler will not let you forget the checks. This is a powerful mechanism whose value is hard to overstate.

// “?” permits undefined, and “| null” permits null
function someFunction (value?: number | null) {
  if (value == null) {
    return 0
  }
  return value * 2
}
Enter fullscreen mode Exit fullscreen mode

There is only one potentially dangerous point. Everything works perfectly in our statically typed code. But applications often work with dynamically received data, such as data from a server, whose declared types may not match reality at all. In other words, the types say a value cannot be null, but the server sends it anyway. You can avoid this by making validation of server responses standard practice, for example with JSON Schema validators.

External libraries can be a little more treacherous. In particular, libraries originally written in JavaScript whose typings are maintained separately from the code. In those cases, mismatches between the real code and its types can produce exactly the situation described above.

Here is a compatibility table for types with strictNullChecks on and off: Any, unknown, object, void, undefined, null and never assignability.

strictPropertyInitialization

Recommended: always / difficulty: easy / related to strictNullChecks.

strictPropertyInitialization ensures that declared class properties are always initialized:

class User {
  name: string
  // email is initialized neither here nor in the constructor
  // the compiler will tell us that a value must be set
  email: string
  constructor (name: string) {
    this.name = name
  }
}
Enter fullscreen mode Exit fullscreen mode

This option is a kind of companion to strictNullChecks, so it works only when that flag is also enabled.

strictFunctionTypes

Recommended: always / difficulty: easy.

Setting strictFunctionTypes: true enables stricter checking of function signatures. Overall, this option looks like an obvious must-have:

interface StringOrNumberFunc {
  (value: string | number): void
}
function someFunction (value: string) {
  console.log(value)
}
// the signatures do not match
// string | number is not equivalent to string
let func: StringOrNumberFunc = someFunction
func(10)
func('10')
Enter fullscreen mode Exit fullscreen mode

noImplicitThis

Recommended: always / difficulty: easy.

When you use this, this flag checks that the execution context is known. Consider this example:

class SomeClass {
  multiplier: number = 5
  createSomeFunction (value: number) {
    return function () {
      // context is lost: this is NOT a SomeClass instance here
      return value * this.multiplier
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The this object is unknown because function does not pass the context through automatically. Here you can fix it by replacing function with an arrow function.

Consider a standalone function that, for some reason, must use an external this:

function sayHello (name: string) {
  console.log(this.helloWord + ' ' + name)
}
Enter fullscreen mode Exit fullscreen mode

Here, too, this is unknown. You can run a function with an external this using bind, call, or apply. You can teach the function about its context like this:

// state the this type as the first “argument”
// in practice, it is not counted as a function argument
function sayHello (this: HelloStyle, name: string) {
  console.log(this.helloWord + ' ' + name)
}
// declare greeting variants
interface HelloStyle {
  helloWord: string
}
class HawaiiStyle implements HelloStyle {
  helloWord = 'Aloha'
}
class RussianStyle implements HelloStyle {
  helloWord = 'Привет,'
}
// now call it
sayHello.bind(new HawaiiStyle())('World')
sayHello.call(new RussianStyle(), 'World')
sayHello.apply(new RussianStyle(), ['World'])
Enter fullscreen mode Exit fullscreen mode

In every case, the context type is known, and even editor type hints work as they should.

strictBindCallApply

Recommended: always / difficulty: medium.

strictBindCallApply enables stricter signature checks for the corresponding methods: bind, call, and apply.

function someFunction (value: string) {
  console.log(value)
}
someFunction.call(undefined, '10')
// yes, cases like this need a separate flag
someFunction.call(undefined, false)
Enter fullscreen mode Exit fullscreen mode

Apart from a few specialized cases, this flag does not cause problems. If a difficult case does arise, I recommend disabling the check at the problem location with // @ts-ignore rather than turning the rule off entirely.

Strict Checks: summary

  • Every flag here really is useful. Together they let TypeScript reach its potential and deliver the most value.
  • You can enable them all with strict: true, or enable each one separately. The latter is ideal for mixed codebases where some code has not yet been converted to TypeScript.
  • alwaysStrict is not about TypeScript strict mode. It is about JavaScript strict mode, that is, "use strict".
  • The top priorities are noImplicitAny and strictNullChecks together with strictPropertyInitialization. Next come strictFunctionTypes and noImplicitThis. strictBindCallApply comes last.

The Linter Checks group

The name speaks for itself: these flags are meant to check code quality rather than type compatibility. They are rules of the same kind as ESLint’s. Some Linter Checks rules can indeed be replaced with their ESLint equivalents. The full list is:

{
  "compilerOptions": {
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
  }
}
Enter fullscreen mode Exit fullscreen mode

Every flag above defaults to false. To make things strict, set it to the opposite value: true.

I deliberately separated noPropertyAccessFromIndexSignature and noUncheckedIndexedAccess from the other flags with a blank line. They are here only because conceptually they cannot live in Strict Checks. That section is intended for flags controlled by strict.

But these flags are too specific to enable by default. That is more or less what the announcements for noPropertyAccessFromIndexSignature and noUncheckedIndexedAccess say. Still, that does not make them lint rules. They check types and really belong to Strict Checks, but they are not enabled automatically by strict.

The remaining flags are full citizens of this section and have analogous ESLint rules. There are a few temporary limitations, since not every rule has been moved from TSLint to ESLint yet. You can see the rule mappings in the TSLint Migration Guide and the ESLint rules for TSLint repository.

Let’s look at each flag.

noPropertyAccessFromIndexSignature

Recommended: always / difficulty: easy / one caveat / cannot be replaced by a linter.

noPropertyAccessFromIndexSignature forbids accessing an object property with a dot, also known as dot notation, when the property is not declared explicitly but through arbitrarily named properties (index signatures).

interface User {
  // explicitly declared properties
  login: string
  email: string
  // arbitrary properties
  [key: string]: string
}
const user: User = {
  login: 'hello',
  email: 'hello@example.com'
}
// with noPropertyAccessFromIndexSignature: true
// this line produces an error
const username = user.name
// but this access is still allowed
const username2 = user['name']
Enter fullscreen mode Exit fullscreen mode

Instead of dot notation, we use bracket notation. The idea is much the same as with noImplicitAny: draw attention to potentially troublesome places.

The only reason not to use this option is that you end up with two syntaxes for property access. In my view, that is not a problem. While the syntaxes differ, each has its purpose. Always use dot notation. If an exception comes up, switch to bracket notation. The result is safer code, so I recommend this flag.

There is one more reason, but noUncheckedIndexedAccess addresses it.

noUncheckedIndexedAccess

Recommended: case by case / difficulty: medium / serious caveat / related to strictNullChecks / cannot be replaced by a linter.

Let’s look at the previous example again.

interface User {
  // explicitly declared properties
  login: string
  email: string
  // arbitrary properties
  [key: string]: string
}
const user: User = {
  login: 'hello',
  email: 'hello@example.com'
}
// the username variable has type string
const username = user['name']
Enter fullscreen mode Exit fullscreen mode

As we can see, username has type string. That is not quite right, since we can access any property, even one that does not exist. You could declare arbitrary fields as [key: string]: string | undefined. That solves the problem, but creates manual work. Plus, people will occasionally forget to add undefined.

noUncheckedIndexedAccess does that work for us. The compiler will no longer let you use a variable obtained this way without checking it first.

Here is another example, this time with an array:

const strings: string[] = ['hello']
// here, number will have type string
const number = strings[100]
Enter fullscreen mode Exit fullscreen mode

Obviously, the variable is undefined here. With noUncheckedIndexedAccess enabled, its type in cases like this becomes string | undefined.

It seems perfect: bracket notation marks the object’s “magic” properties. We can use them only after checking for undefined. Everything is safe. What could go wrong?

In short:

const strings: string[] = ['hello']
// here too, number will have type string | undefined
const number = strings[0]
Enter fullscreen mode Exit fullscreen mode

Or:

function upperCaseAll(strings: string[]) {
  for (let i = 0; i < strings.length; i++) {
      // this causes a compilation error
      console.log(strings[i].toUpperCase());
  }
}
Enter fullscreen mode Exit fullscreen mode

In other words, with this configuration you gain safety, but in some cases pay for it with checks that make no sense, solely to tame a stubborn compiler. It feels very much like a workaround.

Code with noUncheckedIndexedAccess is definitely much safer, and that is TypeScript’s main purpose. But the trade-off can be substantial, so I cannot recommend the flag to everyone.

This flag is a kind of companion to strictNullChecks, so it works only when that flag is also enabled.

noImplicitReturns

Recommended: always / difficulty: easy / ESLint rule: consistent-return, not implemented yet.

This flag ensures every branch of a function returns a value:

function lookupHeadphonesManufacturer(color: 'blue' | 'black'): string {
  if (color === 'blue') {
    return 'beats'
  }
  // forgot return
}
Enter fullscreen mode Exit fullscreen mode

It is analogous to ESLint’s consistent-return, which at the time was not yet implemented for TypeScript.

noFallthroughCasesInSwitch

Recommended: if you do not use ESLint / difficulty: easy / ESLint rule: no-fallthrough.

This flag checks for break in a switch/case statement:

switch (value) {
  case 0:
    console.log('even')
    // forgot break
  case 1:
    console.log('odd')
    break
}
Enter fullscreen mode Exit fullscreen mode

It can be replaced by no-fallthrough.

noUnusedLocals

Recommended: for production if you do not use ESLint / difficulty: easy / ESLint rule: no-unused-vars.

The code is checked for unused variables:

function createKeyboard (modelID: number) {
  // an unused variable is declared
  const defaultModelID = 23
  return {
    type: 'keyboard',
    modelID
  }
}
Enter fullscreen mode Exit fullscreen mode

This rule is extremely inconvenient during development. While you are writing code, it may be in pieces, and unused variables will cause compilation errors.

You can override the flag for a development environment or use ESLint’s no-unused-vars.

noUnusedParameters

Recommended: for production if you do not use ESLint / difficulty: easy / ESLint rule: no-unused-vars.

The code is checked for unused function and method arguments:

function createDefaultKeyboard (modelID: number) {
  const defaultModelID = 23
  return {
    type: 'keyboard',
    modelID: defaultModelID
  }
}
Enter fullscreen mode Exit fullscreen mode

Like noUnusedParameters, this rule gets in the way of a comfortable development process. You can override it for development, or add arguments settings to ESLint’s no-unused-vars.

Linter Checks: summary

  • There is no single flag that turns on every option in the section.
  • noPropertyAccessFromIndexSignature and noUncheckedIndexedAccess are really Strict Checks. They are just not automatically enabled by the main strict flag.
  • The other flags, noFallthroughCasesInSwitch, noUnusedLocals, and noUnusedParameters, can be replaced by linter rules. The rule for noImplicitReturns was only temporarily unsupported.

The Advanced group

This is the largest section. It looks as though every option that did not fit a separate group was put here. Some of its options concern compiler strictness:

{
  "compilerOptions": {
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "noImplicitUseStrict": false,
    "suppressExcessPropertyErrors": false,
    "suppressImplicitAnyIndexErrors": false,
    "noStrictGenericChecks": false,
  }
}
Enter fullscreen mode Exit fullscreen mode

allowUnreachableCode and allowUnusedLabels are separated by a blank line for a reason: they escaped from Linter Checks.

Surprisingly, these flags default to true, while strictness requires false. That is the exact opposite.

It is easy enough to remember. Earlier options of this kind used the no prefix: “forbid the allowance.” These use allow: “permit the allowance.” For consistency, they ought to be renamed noUnreachableCode and noUnusedLabels and sent to the appropriate group.

The remaining flags, noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors, and suppressImplicitAnyIndexErrors, could form a new group called Base Strict Checks. Here is why:

These flags default to false and should remain false for strictness!

So we have found the options enabled by default that make up TypeScript’s out-of-the-box type safety.

In essence, the options above (except noImplicitUseStrict) permit explicit violations of typing. That can sometimes serve as a temporary measure while migrating difficult JavaScript code.

In 99.9% of cases, you will never need to override any of these four flags.

Now, a few words about this pseudo-group.

allowUnreachableCode

Recommended: always for production / difficulty: easy / ESLint rule: no-unreachable, not implemented yet.

This flag forbids unreachable code: code written after return, throw, break, or continue.

function fn (n: number) {
  if (n > 5) {
    return true
  } else {
    return false
  }
  // unreachable code
  return true
}
Enter fullscreen mode Exit fullscreen mode

It is convenient to disable this flag during development. It is analogous to ESLint’s no-unreachable, which at the time was not implemented for TypeScript.

allowUnusedLabels

Recommended: for production if you do not use ESLint / difficulty: easy / ESLint rule: no-unused-labels.

This option forbids unused labels. I do not think you will find this construct in the wild nowadays, but still:

function verifyAge (age: number) {
  if (age > 18) {
    // that very unused label
    verified: true
  }
}
Enter fullscreen mode Exit fullscreen mode

Replace it with ESLint’s no-unused-labels.

noImplicitUseStrict

Configured by default. Do not change it / related to alwaysStrict.

This flag automatically adds "use strict" when target is lower than ES6. alwaysStrict does the same for every target. It is hard to imagine ever needing to override this flag.

If both noImplicitUseStrict and alwaysStrict are set to true, compilation fails because the settings conflict.

suppressExcessPropertyErrors

Configured by default. Do not change it.

This flag ensures an object cannot contain properties that were not described in its structure:

interface Point {
  x: number
  y: number
}
const p: Point = {
  x: 1,
  y: 3,
  // property z is not declared in the Point interface
  z: 10
}
Enter fullscreen mode Exit fullscreen mode

You can temporarily switch this setting off while migrating a JavaScript codebase if it has many such cases. But restore the original value afterward. For one-off cases, // @ts-ignore is better.

suppressImplicitAnyIndexErrors

Configured by default. Do not change it / related to noImplicitAny.

This flag checks that bracket notation cannot access object properties which are not declared explicitly or through arbitrary properties. Recall the noUncheckedIndexedAccess example and remove its arbitrary properties:

interface User {
  // explicitly declared properties
  login: string
  email: string
  // comment out the arbitrary properties
  // [key: string]: string
}
const user: User = {
  login: 'hello',
  email: 'hello@example.com'
}
// name can no longer be accessed
const username = user['name']
Enter fullscreen mode Exit fullscreen mode

It might seem that no other behavior is possible. But, as I said, the final four flags can sometimes permit a type-system violation for edge cases. For example, this flag is often inverted when people do not know how to extend TypeScript’s built-in typings or third-party typings. That is bad practice. The right tool is declaration merging.

For example, suppose we load Google Maps through a script tag and try to create a Pin:

const pin = new window.google.maps.Pin(59.9386, 30.3141)
Enter fullscreen mode Exit fullscreen mode

Naturally, this errors because the built-in typings know nothing about google. // @ts-ignore is awkward because you would need it on almost every line. So you will often see this solution: set suppressImplicitAnyIndexErrors: true (allow the escape hatch) and write:

const pin = new window['google']['maps']['Pin'](59.9386, 30.3141)
Enter fullscreen mode Exit fullscreen mode

That can exist in projects halfway through a migration. Still, I would not recommend it even there. Declaration merging solves the problem:

// an arbitrary merging.d.ts file
interface Pin {
  // declaration
}
interface PinConstructor {
  new(lat: number, lng: number): Pin
}
interface Window {
  google: {
    maps: {
      Pin: PinConstructor
    }
  }
}
// usage in code
const pin = new window.google.maps.Pin(59.9386, 30.3141)
Enter fullscreen mode Exit fullscreen mode

The same practice applies to external libraries, for example when extending Express’s req and res objects.

noStrictGenericChecks

Configured by default. Do not change it.

Overriding this flag can make the compiler more “lenient” with generics:

type A = <T, U>(x: T, y: U) => [T, U]
type B = <S>(x: S, y: S) => [S, S]
function f (a: A, b: B) {
  // OK
  b = a
  // this should be an error, but the compiler ignores it
  a = b
}
Enter fullscreen mode Exit fullscreen mode

Advanced: summary

  • The section contains many unrelated flags. Some concern compiler strictness and could be moved to thematic sections.
  • allowUnreachableCode and allowUnusedLabels are linting flags and could move to Linter Checks.
  • noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors, and suppressImplicitAnyIndexErrors are the defaults that provide TypeScript’s built-in type safety.
  • You can forget about the other four flags.
  • noImplicitUseStrict is a variation of alwaysStrict.
  • noStrictGenericChecks, suppressExcessPropertyErrors, and suppressImplicitAnyIndexErrors turn off some basic syntax checks to make difficult JavaScript-to-TypeScript migrations easier.

Conclusion

To finish, here is a cheat sheet of every flag mentioned above for maximum strictness and clarity.

tsconfig-checks.json:

{
  "compilerOptions": {
    // Strict Checks
    "alwaysStrict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "strictBindCallApply": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    // Linter Checks
    "noImplicitReturns": true, // https://eslint.org/docs/rules/consistent-return 🛑
    "noFallthroughCasesInSwitch": true, // https://eslint.org/docs/rules/no-fallthrough
    "noUnusedLocals": true, // https://eslint.org/docs/rules/no-unused-vars
    "noUnusedParameters": true, // https://eslint.org/docs/rules/no-unused-vars#args
    "allowUnreachableCode": false, // https://eslint.org/docs/rules/no-unreachable 🛑
    "allowUnusedLabels": false, // https://eslint.org/docs/rules/no-unused-labels
    // Base Strict Checks
    "noImplicitUseStrict": false,
    "suppressExcessPropertyErrors": false,
    "suppressImplicitAnyIndexErrors": false,
    "noStrictGenericChecks": false,
  }
}
Enter fullscreen mode Exit fullscreen mode

Why tsconfig-checks.json? In part one, we discussed using config inheritance so strictness settings do not get mixed with other options. That is exactly what we will do.

tsconfig.json:

{
  // strictness settings
  "extends": "./tsconfig-checks.json",
  "compilerOptions": {
    // all other settings
  }
}
Enter fullscreen mode Exit fullscreen mode

And for a smoother development experience, so unreachable code and unused variables do not break compilation, you can do this:

tsconfig-dev.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    // disable some linting so compilation does not break
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "allowUnreachableCode": true,
    "allowUnusedLabels": true
  }
}
Enter fullscreen mode Exit fullscreen mode

That is all for now. Next, I plan to dig into optimization and performance options and will be sure to share what I find. See you next time!

This article is based on my thread on the collective @jsunderhood account.

P.S. I also sometimes write about TypeScript and development on @barinbritva.

Top comments (0)