Why
Typescript offers many features over vanilla Javascript. The main selling point of the language has always been type safety. When people migrate their Javascript code bases over to Typescript, it is almost always with the intent of reducing runtime errors.
Typescript's strictness makes it less likely that you will write unsafe code, but there are still ways that a developer can forfeit runtime safety.
The any
Type
The most common source of runtime errors in Typescript is the any
type, which
essentially turns off the type checker. If your Typescript codebase sees null
pointer exceptions or errors such as undefined is not a function
, chances are,
there is an any
type floating around the codebase somewhere.
The any
type should be avoided at all costs, as it negates the benefits of
structural typing. Despite this, there are a number of reasons that any
appears in a codebase (they are all unfortunate):
- You are migrating a legacy application to TypeScript and it would not be feasible to declare types for all pre-existing data structures.
- You use a third party library that has poor type or missing type coverage.
- You are handling data that cannot be inspected until runtime (example: passing
unsafe strings to
JSON.parse()
, I/O operations, untyped event emitters, etc..)
If you wonder why Typescript allows the any
type at all, it is important to
remember that Typescript is a 100% interoperable superset of Javascript. It
would not be possible to completely remove the any
type while also staying
compatible with the underlying ecosystem and runtime.
A new type was added to Typescript 3.0 and it can help developers deal with realworld tradeoffs such as the ones listed above. Spoiler alert: it's the one I mentioned in the title.
The unknown
Type
Typescript 3.0 shipped with a new unknown
type, which is a stricter, more
paranoid version of the any
type. Like the any
type, it can be used in
places where runtime structure is not known. Unlike the any
type, it will not
allow you to accidentally call non-existent properties on objects (or worse,
call a function that is not a function at all).
Here's an unsafe example of any
:
function inspectFooBar(x: any) {
// *runtime* type error:
// `TypeError: undefined is not a function`
console.log(x.foo.bar());
}
In the example above, the Typescript compiler is trusting us to ensure that the
x
parameter has a callable foo.bar()
property. Keeping track of that fact in
a large codebase can be challenging (or impossible).
The code below shows a safer, more paranoid version that uses the uknown
type.
Although both of these examples are invalid, the example below is preferable
because the error happens at compile time rather than runtime:
function inspectFooBar(x: unknown) {
// Object is of type 'unknown'.
console.log(x.foo.bar());
}
Although the example above is safer, it would appear that the unknown
type is
useless- what point is there in having a type with inaccessible properties? This
is not the case. When used in conjunction with
user defined type guards,
the unknown
type is a useful tool for safe programming. The following section
requires a working knowledge of user defined type guards, which are explained in
the
Typescript Documentation.
Combining unknown
with Type Guards
A common pattern
// Helper interface that describes an object with
// unknown attributes.
type ObjectLike = { [key: string]: unknown };
const isUnknownObject = (
x: unknown,
): x is ObjectLike => (x !== null && typeof x === "object");
interface User {
firstName: string;
lastName: string;
}
function isUser(user: unknown): user is User {
if (isUnknownObject(user)) {
const hasFirst = typeof user.first === "string";
const hasLast = typeof user.last === "string";
return hasFirst && hasLast;
}
return false;
}
Using unknown
in Type Guards
interface User {
name: {
first: string;
last: string;
};
}
function isUser(u: any): u is User {
if (u && u.name) {
const hasFirst = (u as User).name.first !== undefined;
const hasLast = (u as User).name.last !== undefined;
return hasFirst && hasLast;
}
return false;
}
The example above is simple and would be unlikely to cause confusion. As the
application changes, or as the User
interface begins to take no more
responsibilities, the function has the propensity to cause problems. Most
notably:
- It uses the
any
type, which inevitably leads to misfortune. - It assumes that you "Know What You're Doing™️", even as the interface evolves. Example: You decide to make some fields optional (or rename them) six months later.
- It requires two typecasts. Although typecasts are not as bad as the
any
type, they still can lead to runtime errors.
Other Safety Precautions
- Refactor jumbled data structures into TaggedUnions
- Set
"strict": true
intsconfig.json
(Don't add this, maybe?) Avoiding The object
Type
interface ObjectLike {
[key: string]: unknown;
}