Removing explicit any from a TypeScript codebase
Much has been written about the any
type (more of a type black hole than a type) and why the use of it is usually bad, yet many teams frequently adopt the use of any
- especially when migrating a JavaScript codebase to TypeScript. At some point along that journey teams may want to remove the uses of any
to more effectively leverage the safety that TypeScript provides. While they could just do a global search and replace and map any
to unknown
this usually isn’t a viable strategy as this will cause an explosion of type errors in the code. Migrating from any
to unknown
will fail type checks even with strict mode off.
A much gentler approach is to remove the explicit usage of any
and let it fall through to an implicit any
. An implicit any
is how the TypeScript compiler will type something when it cannot infer a type. You may be asking yourself, why is an implicit any better than an explicit any? From a soundness point of view, it isn’t. From a practical point of view, it lets you do some interesting things to help you ratchet up type safety. The explicit use of any
can never result in a type error (because the compiler just ignores it), but you can configure the compiler to error on the usage of implicit any by turning strict mode on or by setting noImplicitAny: true
in your tsconfig.
If it is not obvious why this is useful, I highlight the challenges of adopting TypeScript on a non-trivial codebase here (coming soon).
An Unexpected Puzzle
It turns out that deleting all references of any is not as simple as you might assume. Even directly replacing any
with unknown
is not trivial (but certainly easier) as a blanket match on the word any may replace code comments, test descriptions etc.
You might assume that you could search for a pattern like : any
and replace it with an empty string. However, consider what happens to the following function in this case
const fn = (foo: any[]): any[] => foo;
const fn2 = (foo: any | undefined): any | undefined => foo;
// If we remove ": any" we get the following
const brokenFn = (foo[])[] => foo;
const brokenFn2 = (foo | undefined) | undefined => foo;
Then you also have things like aliases, generics, optional arguments and more to consider. A non-exhaustive list of some different usages of any:
// standard usage
fn = (x: any): any => x;
// optionals
fn = (x?: any) => x;
fn = (x?: any[]) => x;
// arrays
fn = (x: any[]) => x;
fn = (x: any[][]) => x;
fn = (x: any[][][]) => x;
// unions
fn = (x: any[] | undefined) => x;
fn = (x: any[] | null) => x;
fn = (x: any | undefined) => x;
fn = (x: any | null) => x;
fn = (x: any | string) => x;
// type alias
type DefinitelyNotAnAny = any;
const foo: DefinitelyNotAnAny = { good: "luck" };
// Generics
fn = async (): Promise<any> | void => {
/** some code here */
};
fn = async (): Promise<any> | any => {
/** some code here */
};
fn = async (): Promise<any>[] => {
/** some code here */
};
fn = async (): Promise<any> => {
/** some code here */
};
fn = async (): Promise<any | undefined> => {
/** some code here */
};
fn = async (): Promise<any | null> => {
/** some code here */
};
fn = async (): Promise<any[]> => {
/** some code here */
};
fn = (x: Array<any>) => x;
fn = <any>() => {
/** some code here */
};
fn = <any, any>() => {
/** some code here */
};
fn = <any, any>() => {
/** some code here */
};
fn = <any, any, any>() => {
/** some code here */
};
// function types
type asyncFn = () => Promise<any>;
type asyncFn = () => Promise<any[]>;
type fn = () => any;
// Other
const x = {} as any;
const xs = [] as any[];
I have a Github repo that tackles this problem and includes a script, using sed
, to go and delete as many different usage patterns of any
that I could think of. The script as it stands looks something like this (but check the repo for live updates)
find "$DIR" \( -name '*.tsx' -o -name '*.ts' \) -exec sed -i "" \
'
s/?: any\[\]/?/g;
s/: any\[\] | undefined//g;
s/: any\[\] | null//g;
s/= any;/= unknown;/g
s/: Promise<any> | void//g;
s/: Promise<any> | any//g;
s/: Promise<any>\[\]//g;
s/: Promise<any>//g;
s/=> Promise<any>/=> Promise<unknown>/g;
s/: Promise<any\[\]>//g;
s/=> Promise<any\[\];/=> Promise<unknown\[\]/g;
s/: Array<any>//g;
s/<any>//g;
s/<any\[\]>//g;
s/=> any;/=> unknown;/g;
s/<any | undefined>//g;
s/: any | undefined//g;
s/<any | null>//g;
s/: any | null//g;
s/: any | string//g;
s/?: any/?/g;
s/: any\[\]\[\]\[\]//g;
s/: any\[\]\[\]//g;
s/: any\[\]//g;
s/=> any\[\]/=> unknown\[\]/g;
s/=> any/=> unknown/g;
s/{ \[key\: string\]: any }/Record<string, unknown>/g;
s/\[key\: string\]: any;//g;
s/: any//g;
s/<any/<unknown/g;
s/\[any,/\[unknown,/g;
s/, any,/, unknown,/g;
s/any>/unknown>/g;
s/ as any\[\]//g;
s/ as any//g;
' \
{} +