How strict is Typescript's strict mode?

briwa

briwa

Posted on June 1, 2019

How strict is Typescript's strict mode?

I started out writing code in Javascript without proper typing. When I switched to Typescript, I migrated my code without turning the strict mode on because I knew that the typing refactor was going to be too much to handle, and I should focus on passing the unit tests first.

Even without the strict mode, it was already a shift of paradigm because you have to specifically define most of the things unlike Javascript. I thought I've already strict enough with my types back then. But how strict is strict mode?

According to the docs, when Typescript strict mode is set to on, it will validate your code using the strict type rules under the 'strict' family to all files in the project. The rules are:

  • noImplicitAny
  • noImplicitThis
  • strictNullChecks
  • strictPropertyInitialization
  • strictBindCallApply
  • strictFunctionTypes

These are some of the lessons I learned when I bumped into these rules.

1. noImplicitAny

This rule disallows variables or function arguments to have an implicit any type. Consider this example:

// Javascript/Typescript non-strict mode
function extractIds (list) {
  return list.map(member => member.id)
}

Looking at the code, list can be anything. Sure, from the .map you would think that it is an array of members, and the member has property called id but there isn't anything that specifically defines that. This is why it is an error in strict mode.

// Typescript strict mode
function extractIds (list) {
  //              ❌ ^^^^
  //                 Parameter 'list' implicitly has an 'any' type. ts(7006)
  return list.map(member => member.id)
  //           ❌ ^^^^^^
  //              Parameter 'member' implicitly has an 'any' type. ts(7006)
}

A fixed version would be:

// Typescript strict mode
interface Member {
  id: number
  name: string
}

function extractIds (list: Member[]) {
  return list.map(member => member.id)
}

Another common code that you might see in the wild:

// Typescript strict mode
function onChangeCheckbox (e) {
  //                    ❌ ^
  //                       Parameter 'e' implicitly has an 'any' type. ts(7006)
  e.preventDefault()
  const value = e.target.checked
  validateCheckbox(value)
}

You can use some of the global types defined by the Typescript itself for, in this case, browser-specific types:

// Typescript strict mode
interface ChangeCheckboxEvent extends MouseEvent {
  target: HTMLInputElement
}

function onChangeCheckbox (e: ChangeCheckboxEvent) {
  e.preventDefault()
  const value = e.target.checked
  validateCheckbox(value)
}

Do note that it would also throw an error if you import libraries that has no type definitions, because that would imply that the imported library has an any type.

// Typescript strict mode
import { Vector } from 'sylvester'
//                  ❌ ^^^^^^^^^^^
//                     Could not find a declaration file for module 'sylvester'.
//                     '/foo/node_modules/sylvester/lib/node-sylvester/index.js' implicitly has an 'any' type.
//                     Try `npm install @types/sylvester` if it exists
//                     or add a new declaration (.d.ts) file containing `declare module 'sylvester';` ts(7016)

It can be a bottleneck in your project since you might end up having to write that type definitions yourself if there isn't any, but having all types defined should've been the right thing to do anyway, at least in strict mode.

2. noImplicitThis

This rule disallows this context to be implicitly defined. Consider this example:

// Javascript/Typescript non-strict mode
function uppercaseLabel () {
  return this.label.toUpperCase()
}

const config = {
  label: 'foo-config',
  uppercaseLabel
}

config.uppercaseLabel()
// FOO-CONFIG

For someone who's been writing Javascript, it is known that this refers to the config object, so this.label would just be retrieving config.label, which is why this code works. However, referring to this on a function can be ambiguous.

// Typescript strict mode
function uppercaseLabel () {
  return this.label.toUpperCase()
  //  ❌ ^^^^
  //     'this' implicitly has type 'any' because it does not have a type annotation. ts(2683)
}

If we run uppercaseLabel alone, it would throw an error because this context is not on config anymore, hence the error because label is undefined.

One way to fix it is to avoid using this on a function without a context:

// Typescript strict mode
const config = {
  label: 'foo-config',
  uppercaseLabel () {
    return this.label.toUpperCase()
  }
}

Typescript won't even complain on this because all types are inferred properly. Or, even better, write the interface, so that all types are now defined instead of inferred.

// Typescript strict mode
interface MyConfig {
  label: string
  uppercaseLabel: (params: void) => string
}

const config: MyConfig = {
  label: 'foo-config',
  uppercaseLabel () {
    return this.label.toUpperCase()
  }
}

3. strictNullChecks

This rule validates the possibility of values returning null or undefined. Consider this example:

// Javascript/Typescript non-strict mode
function getArticleMetaById (articles: Article[], id: string) {
  const article = articles.find(article => article.id === id)
  return article.meta
}

Now, of course I would have checked the code first in the browser if it works (and it did). However, in strict mode, Typescript would remind me that there are chances that .find would return undefined when none of the ids in the responses would match the given id.

// Typescript strict mode
function getArticleMetaById (articles: Article[], id: string) {
  const article = articles.find(article => article.id === id)
  return article.meta
  //  ❌ ^^^^^^^
  //     Object is possibly 'undefined'. ts(2532)
}

This would actually broaden my code specification so that now I have to actually handle error cases as well, which should've been done in the first place.

// Typescript strict mode
function getArticleMetaById (articles: Article[], id: string) {
  const article = articles.find(article => article.id === id)
  if (typeof article === 'undefined') {
    throw new Error(`Could not find an article with id: ${id}.`)
  }

  return article.meta
}

4. strictPropertyInitialization

This rule validates properties in a class to be initialized either inside a constructor function or already defined before constructed. Consider this example:

// Javascript
class Student {
  constructor (grade, lessons) {
    this.grade = grade
    this.lessons = lessons.filter(lesson => lesson.grade <= grade)
  }

  setRedoLessons (lessons) {
    this.redoLessons = lessons
  }
}

With Typescript, all class instance properties can be defined properly.

// Typescript non-strict mode
interface Lesson {
  title: string
  grade: number
}

class Student {
  private grade: number
  private lessons: Lesson[]
  private redoLessons: Lesson[]
  private greetingType: string
  constructor (grade: number, lessons: Lesson[]) {
    this.grade = grade
    this.lessons = lessons.filter(lesson => lesson.grade <= grade)
  }

  setRedoLessons (lessons: Lesson[]) {
    this.redoLessons = lessons
  }
}

However, at this point you couldn't tell whether there is a property that isn't defined either in the constructor function or in some method. I don't know if you noticed but in the previous code I sneaked in a property that meets such criteria.

// Typescript non-strict mode
interface Lesson {
  title: string
  grade: number
}

class Student {
  private grade: number
  private lessons: Lesson[]
  private redoLessons: Lesson[]
  private greetingType: string // 👀 This is undefined, not used and there's no error!
  constructor (grade: number, lessons: Lesson[]) {
    this.grade = grade
    this.lessons = lessons.filter(lesson => lesson.grade <= grade)
  }

  setRedoLessons (lessons: Lesson[]) {
    this.redoLessons = lessons
  }
}

In strict mode, it actually throws errors on all undefined properties not defined in the constructor.

// Typescript strict mode
interface Lesson {
  title: string
  grade: number
}

class Student {
  private grade: number
  private lessons: Lesson[]
  private redoLessons: Lesson[]
  //   ❌ ^^^^^^^^^^^
  //      Property 'redoLessons' has no initializer and is not definitely assigned in the constructor. ts(2564)
  private greetingType: string
  //   ❌ ^^^^^^^^^^^^
  //      Property 'greetingType' has no initializer and is not definitely assigned in the constructor. ts(2564)
  constructor (grade: number, lessons: Lesson[]) {
    this.grade = grade
    this.lessons = lessons.filter(lesson => lesson.grade <= grade)
  }

  setRedoLessons (lessons: Lesson[]) {
    this.redoLessons = lessons
  }
}

This helps you review the code and see whether the properties are indeed being used in places other than the constructor. If it is, you can put an ! on it and simply remove those who aren't.

// Typescript strict mode
interface Lesson {
  title: string
  grade: number
}

class Student {
  private grade: number
  private lessons: Lesson[]
  private redoLessons!: Lesson[]
  constructor (grade: number, lessons: Lesson[]) {
    this.grade = grade
    this.lessons = lessons.filter(lesson => lesson.grade <= grade)
  }

  setRedoLessons (lessons: Lesson[]) {
    this.redoLessons = lessons
  }
}

However, I would recommend to either set it to a default value if it's not defined in the constructor as a good practice, otherwise it would be forever undefined until it is set (unless that is intentional).

5. strictBindCallApply

This rule validates the usage of bind, call or apply as defined in the function. Consider this example:

// Typescript without strict mode
function sum (num1: number, num2: number) {
  return num1 + num2
}

sum.apply(null, [1, 2])
// 3

Maybe a mistake is made, thinking that the sum can take in more than two arguments. When the code is run, there is no error thrown on Typescript and in your environment (browser, perhaps).

// Typescript non-strict mode
function sum (num1: number, num2: number) {
  return num1 + num2
}

sum.apply(null, [1, 2, 3])
// Still 3...?

The only way to know if it's a mistake is when the code is tested manually or in a unit test. In strict mode, you can spot this even before that:

// Typescript strict mode
function sum (num1: number, num2: number) {
  return num1 + num2
}

sum.apply(null, [1, 2, 3])
//           ❌ ^^^^^^^^^
//              Argument of type '[number, number, number]' is not assignable to parameter of type '[number, number]'.
//                Types of property 'length' are incompatible.
//                  Type '3' is not assignable to type '2'. ts(2345)

Then it might be a good time to rethink of the sum function design.

// Typescript strict mode
function sum (...args: number[]) {
  return args.reduce<number>((total, num) => total + num, 0)
}

sum.apply(null, [1, 2, 3])
// 6

6. strictFunctionTypes

Unfortunately, I have yet to find the use case of this rules in my code so far, so I can't comment much on it. You can always check out the release notes for strictFunctionTypes on the docs. If anyone has a use case to share, let me know!


If you want to take Typescript restrictions to a different level, I recommend using tslint, though I would say some of the rules are based on preferences, but there are a lot of useful ones. Or, avoid bikeshedding by choosing a standard such as gslint or tslint-config-standard.

I hope you find this article useful! Thanks for reading.


Cover image by by Mark Duffel on Unsplash.

💖 💪 🙅 🚩
briwa
briwa

Posted on June 1, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related