Article summary
Working with JavaScript means working with undefined
. It’s a standard way to say, “This thing you asked for doesn’t exist.”
Thinking about it all the time tends to break brains, though — and not thinking about it introduces bugs. Thankfully, TypeScript is a great tool for helping you deal with it and writing better code in the process.
What’s undefined?
A project set up with TypeScript’s strict flag will check for all kinds of potential issues in your code. I recommend letting TypeScript be as strict as you can.
undefined
typically shows up in a handful of key places:
- An uninitialized or absent property of an object
- A potentially-omitted optional argument to a function
- A return value to indicate something that was requested is missing
- A potentially-uninitialized variable
TypeScript has tools to deal with all of these.
You must tell TypeScript if a property is optional.
I don’t think you can program JavaScript without having seen undefined is not a function
at least once in your life — and once seems far too small a number.
When you have a JavaScript object and you ask for a property that doesn’t exist, JavaScript will return undefined
rather than throwing an error.
In strict mode, this means a couple of things. First, if you don’t tell TypeScript that a property is optional, it will expect it to be set.
type Foo = {
bar: number;
}
const a: Foo = {}; // This is an error:
// Property 'bar' is missing in type '{}' but required in type 'Foo'.
// ts(2741)
const b: Foo = { bar: 11 } // This works!;
Adding ?
to the property name on a type, interface, or class definition will mark that property as optional.
type Foo = {
bar?: number;
}
const a: Foo = {}; // This is now OK!
const b: Foo = { bar: 11 }; // This is still OK.
const c: Foo = { bar: undefined }; // This is also OK, somehow…?
c
’s case is interesting. If you hover over Foo
in an IDE, you’ll see TypeScript has actually defined bar
as number | undefined
now.
Even though a
and c
are different objects, the result of asking for a.bar
and c.bar is the same: undefined
.
It’s optional. Now what?
Of course, when you have an optional property, TypeScript forces you to deal with it.
type Foo = {
bar?: number;
}
function addOne(foo: Foo): number {
return foo.bar + 1; // This is an error:
// Object is possibly 'undefined'. ts(2532)
}
You can deal with it in several ways, but the best way is the same way you’d deal with it in JavaScript: by checking to see if what you got is what you expected.
TypeScript understands a number of these kinds of checks and can use them to narrow the picture of what types can possibly make it through to a specific bit of code.
We can use a typeof
check against the bar
property to see if it is undefined
.
function addOne(foo: Foo): number {
if (typeof foo.bar !== 'undefined') {
return foo.bar + 1;
}
throw new Error('bar is undefined');
}
This not only supports our a
object from above, which has no bar
property, but also the c
object, which supports the undefined
protocol for indicating bar
is, well, undefined.
TypeScript will also look at this code and, when inside the if
clause, will have narrowed the type of the bar
property to number
.
TypeScript will also let you “get away” with a truthiness check, like this:
function addOne(foo: Foo): number {
if (foo.bar) {
return foo.bar + 1;
}
throw new Error('bar is undefined');
}
Beware, though: this code has a sneaky bug. 0
is falsy. It will throw if you pass it { bar: 0 }
.
Functions and methods can have optional arguments.
Functions and methods can have optional arguments, just as types, interfaces, and classes can have optional properties. Those are also marked with ?
:
function add(a: number, b?: number): number { … }
We actually don’t have much to cover here on how to handle b
in this case. Because it will be undefined
if not supplied by the caller, it has the type number | undefined
just like our optional properties did, and so we can use the same kind of type guard to handle it.
I’ve just changed the flow a little bit to illustrate that TypeScript’s flow-control analysis is pretty flexible:
function add(a: number, b?: number): number {
if (typeof b === 'undefined') return a;
return a + b;
}
Return values when something is missing.
undefined
can also come back from a number of core language calls. Strict TypeScript spots the potential bug here:
function hello(who: string): string {
return 'Hello, ' + who;
}
function helloStartingWith(letter: string): string {
const people = ['Alice', 'Bob', 'Carol'];
const person = people.find(name => name.startsWith(letter));
return hello(person); // This is the error:
// Argument of type 'string | undefined' is not assignable to
// parameter of type 'string'.
// Type 'undefined' is not assignable to type 'string'.ts(2345)
}
The problem is that person
is actually not typed string
but string | undefined
. That’s because Array.prototype.find
will return undefined
if something’s not found.
Once you have the possibility of undefined
, you know how to handle it. See above.
Make life easier with optional chaining.
There are some neat features in modern TypeScript (and modern JavaScript, too) that can make your life even easier. For example, say you have a slightly more complicated type, like this:
type Foo = {
bar?: Bar
}
type Bar = {
baz?: Baz
}
type Baz = {
qux?: number
}
When things were less deeply-nested, we could do a single typeof
check. But look at this expression:
foo.bar?.baz?.qux
It’s guaranteed to be either a number
or undefined
. It’ll be the latter if any of bar
, baz
, or qux
or missing or undefined
themselves, or the number
value of qux
if it reaches the end of the expression with all properties present.
This is called optional chaining, and it works by stopping evaluation when it reaches either undefined
or null
.
This example is a bit contrived, to be sure. But, particularly in JavaScript frameworks where property accesses on things that are possibly not yet initialized are common, or for writing lambda expressions that would otherwise become thick with type guards, ?.
is a godsend for simplifying code.
Asserting presence
When it comes to classes, TypeScript’s analysis can flag properties that aren’t definitively initialized, and this can save you some pain. It can also cause some pain if the framework you’re using is guaranteed to set those properties before your code will run.
While you can set these properties as optional with ?
to make the compiler happy, you’ll make yourself unhappy with all the type guards you’ll then have to write.
If you’re sure that these items will be initialized, you can instead use !
to assert that this property will be set, and TypeScript will assume you know what you’re talking about.
class Foo {
bar!: number; // This is OK, but
baz: number; // This isn't:
// Property 'baz' has no initializer and is not definitely
// assigned in the constructor. ts(2564)
}
Dealing with optionality isn’t optional.
You have no choice but to deal with optionality and undefined
in JavaScript, but the great news is that there are a lot of tools available with which to deal with them. TypeScript has helped make my JavaScript code so much more robust than it had ever been before, and the continued development of the language has been making everything even better all the time.