How strict is Typescript's strict mode?
briwa
Posted on June 1, 2019
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.
Posted on June 1, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.