Item 41: Understand Evolving any

This feature was introduced way back in TypeScript 2.1 in 2016. The term "evolving any" is not widely used outside the TypeScript compiler itself, but I find it useful to have a name for this unusual pattern.

In TypeScript a variable's type is generally determined when it is declared. After this, it can be refined (by checking if it is null, for instance), but it cannot expand to include new values. There is one notable exception to this, however, involving any types.

In JavaScript, you might write a function to generate a range of numbers like this:

function range(start, limit) {
const out = [];
for (let i = start; i < limit; i++) {
out.push(i);
}
return out;
}

When you convert this to TypeScript, it works exactly as you'd expect:

function range(start: number, limit: number) {
const out = [];
for (let i = start; i < limit; i++) {
out.push(i);
}
return out; // Return type inferred as number[]
}

Upon closer inspection, however, it's surprising that this works! How does TypeScript know that the type of out is number[] when it's initialized as [], which could be an array of any type?

Inspecting each of the three occurrences of out to reveal its inferred type starts to tell the story:

function range(start: number, limit: number) {
const out = []; // Type is any[]
for (let i = start; i < limit; i++) {
out.push(i); // Type of out is any[]
}
return out; // Type is number[]
}

The type of out starts as any[], an undifferentiated array. But as we push number values onto it, its type "evolves" to become number[].

This is distinct from narrowing (Item 22). An array's type can expand by pushing different elements onto it:

const result = [];  // Type is any[]
result.push('a');
result // Type is string[]
result.push(1);
result // Type is (string | number)[]

With conditionals, the type can even vary across branches. Here we show the same behavior with a simple value, rather than an array:

let val;  // Type is any
if (Math.random() < 0.5) {
val = /hello/;
val // Type is RegExp
} else {
val = 12;
val // Type is number
}
val // Type is number | RegExp

A final case that triggers this "evolving any" behavior is if a variable is initially null. This often comes up when you set a value in a try/catch block:

let val = null;  // Type is any
try {
somethingDangerous();
val = 12;
val // Type is number
} catch (e) {
console.warn('alas!');
}
val // Type is number | null

Interestingly, this behavior only happens when a variable's type is implicitly any with noImplicitAny set! Adding an explicit any keeps the type constant:

let val: any;  // Type is any
if (Math.random() < 0.5) {
val = /hello/;
val // Type is any
} else {
val = 12;
val // Type is any
}
val // Type is any

This behavior can be confusing to follow in your editor since the type is only "evolved" after you assign or push an element. Inspecting the type on the line with the assignment will still show any or any[].


If you use a value before any assignment to it, you'll get an implicit any error:

function range(start: number, limit: number) {
const out = [];
// ~~~ Variable 'out' implicitly has type 'any[]' in some
// locations where its type cannot be determined
if (start === limit) {
return out;
// ~~~ Variable 'out' implicitly has an 'any[]' type
}
for (let i = start; i < limit; i++) {
out.push(i);
}
return out;
}

Put another way, "evolving" any types are only any when you write to them. If you try to read from them while they're still any, you'll get an error.

Implicit any types do not evolve through function calls. The arrow function here trips up inference:

function makeSquares(start: number, limit: number) {
const out = [];
// ~~~ Variable 'out' implicitly has type 'any[]' in some locations
range(start, limit).forEach(i => {
out.push(i * i);
});
return out;
// ~~~ Variable 'out' implicitly has an 'any[]' type
}

In cases like this, you may want to consider using an array's map and filter methods to build arrays in a single statement and avoid iteration and evolving any entirely. See Items 23 and 27.

Evolving any comes with all the usual caveats about type inference. Is the correct type for your array really (string|number)[]? Or should it be number[] and you incorrectly pushed a string? You may still want to provide an explicit type annotation to get better error checking instead of using evolving any.

Things to Remember

  • While TypeScript types typically only refine, implicit any and any[] types are allowed to evolve. You should be able to recognize and understand this construct where it occurs.
  • For better error checking, consider providing an explicit type annotation instead of using evolving any.
Like this post? Consider subscribing to my newsletter, the RSS feed, or following me on Twitter.
Effective TypeScript Book Cover

Effective TypeScript shows you not just how to use TypeScript but how to use it well. The book's 62 items help you build mental models of how TypeScript and its ecosystem work, make you aware of pitfalls and traps to avoid, and guide you toward using TypeScript’s many capabilities in the most effective ways possible. Regardless of your level of TypeScript experience, you can learn something from this book.

After reading Effective TypeScript, your relationship with the type system will be the most productive it's ever been! Learn more »