Mon 09 March 2020

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;

}



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 = [];

for ( let i = start; i < limit; i++) {

out.push(i);

}

return out;

}



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 = [];

result.push( 'a' );

result

result.push( 1 );

result



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;

if ( Math .random() < 0.5 ) {

val = /hello/ ;

val

} else {

val = 12 ;

val

}

val



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:

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 ;

if ( Math .random() < 0.5 ) {

val = /hello/ ;

val

} else {

val = 12 ;

val

}

val



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 = [];





if (start === limit) {

return out;



}

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 = [];



range(start, limit).forEach( i => {

out.push(i * i);

});

return out;



}



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