TypeScript claims to be a strongly typed programming language built on top of JavaScript, providing better tooling at any scale. However, includes the type, which can often sneak into a codebase implicitly and lead to a loss of many of TypeScript's advantages. TypeScript any This article explores ways to take control of the type in TypeScript projects. Get ready to unleash the power of TypeScript, achieving ultimate type safety and improving code quality. any Disadvantages of Using Any in TypeScript TypeScript provides a range of additional tooling to enhance developer experience and productivity: It helps catch errors early in the development stage. It offers excellent auto-completion for code editors and IDEs. It allows for easy refactoring of large codebases through fantastic code navigation tools and automatic refactoring. It simplifies understanding of a codebase by providing additional semantics and explicit data structures through types. However, as soon as you start using the type in your codebase, you lose all the benefits listed above. The type is a dangerous loophole in the type system, and using it disables all type-checking capabilities as well as all tooling that depends on type-checking. As a result, all the benefits of TypeScript are lost: bugs are missed, code editors become less useful, and more. any any For instance, consider the following example: function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any In the code above: You will miss auto-completion inside the function. When you type in your editor, you won't be given correct suggestions for the available methods for . parse data. data In the first case, there is a error because we passed a number instead of a string. TypeScript is not able to highlight the error because disables type checking. TypeError: data.split is not a function any In the second case, the variable also has the type. This means that a single usage of can have a cascading effect on a large portion of a codebase. res2 any any Using is okay only in extreme cases or for prototyping needs. In general, it is better to avoid using to get the most out of TypeScript. any any Where the Any Type Comes From It's important to be aware of the sources of the type in a codebase because explicitly writing is not the only option. Despite our best efforts to avoid using the type, it can sometimes sneak into a codebase implicitly. any any any There are four main sources of the type in a codebase: any Compiler options in tsconfig. TypeScript's standard library. Project dependencies. Explicit use of in a codebase. any I have already written articles on and for the first two points. Please check them out if you want to improve type safety in your projects. Key Considerations in tsconfig Improving Standard Library Types This time, we will focus on automatic tools for controlling the appearance of the type in a codebase. any Stage 1: Using ESLint is a popular static analysis tool used by web developers to ensure best practices and code formatting. It can be used to enforce coding styles and find code that doesn't adhere to certain guidelines. ESLint ESLint can also be used with TypeScript projects, thanks to plugin. Most likely, this plugin has already been installed in your project. But if not, you can follow the official . typesctipt-eslint getting started guide The most common configuration for is as follows: typescript-eslint module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, }; This configuration enables to understand TypeScript at the syntax level, allowing you to write simple eslint rules that apply to manually written types in a code. For example, you can forbid the explicit use of . eslint any The preset contains a carefully selected set of ESLint rules aimed at improving code correctness. While it's recommended to use the entire preset, for the purpose of this article, we will focus only on the rule. recommended no-explicit-any no-explicit-any TypeScript's strict mode prevents the use of implied , but it doesn't prevent from being explicitly used. The rule helps to prohibit manually writing anywhere in a codebase. any any no-explicit-any any // ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {} The primary purpose of this rule is to prevent the use of throughout the team. This is a means of strengthening the team's agreement that the use of in the project is discouraged. any any This is a crucial goal because even a single use of can have a cascading impact on a significant portion of the codebase due to . However, this is still far from achieving ultimate type safety. any type inference Why no-explicit-any is Not Enough Although we have dealt with explicitly used , there are still many implied within a project's dependencies, including npm packages and TypeScript's standard library. any any Consider the following code, which is likely to be seen in any project: const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any Both variables and were implicitly given the type. Neither nor TypeScript's strict mode will warn us in this case. Not yet. pokemons settings any no-explicit-any This happens because the types for and come from TypeScript's standard library, where these methods have an explicit annotation. We can still manually specify a better type for our variables, but there are nearly 1,200 occurrences of in the standard library. It's nearly impossible to remember all the cases where can sneak into our codebase from the standard library. response.json() JSON.parse() any any any The same goes for external dependencies. There are many poorly typed libraries in npm, with most still being written in JavaScript. As a result, using such libraries can easily lead to a lot of implicit in a codebase. any Generally, there are still many ways for to sneak into our code. any Stage 2: Enhancing Type Checking Capabilities Ideally, we would like to have a setting in TypeScript that makes the compiler complain about any variable that has received the type for any reason. Unfortunately, such a setting does not currently exist and is not expected to be added. any We can achieve this behavior by using the type-checked mode of the plugin. This mode works in conjunction with TypeScript to provide complete type information from the TypeScript compiler to ESLint rules. With this information, it is possible to write more complex ESLint rules that essentially extend the type-checking capabilities of TypeScript. For instance, a rule can find all variables with the type, regardless of how was obtained. typescript-eslint any any To use type-aware rules, you need to slightly adjust ESLint configuration: module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, }; To enable type inference for , add to ESLint configuration. Then, replace the preset with . The latter preset adds about 17 new powerful rules. For the purpose of this article, we will focus on only 5 of them. typescript-eslint parserOptions recommended recommended-type-checked no-unsafe-argument The rule searches for function calls in which a variable of type is passed as a parameter. When this happens, type-checking is lost, and all the benefits of strong typing are also lost. no-unsafe-argument any For example, let's consider a function that requires an object as a parameter. Suppose we receive JSON, parse it, and obtain an type. saveForm any // ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`. When we call the function with this parameter, the rule flags it as unsafe and requires us to specify the appropriate type for the variable. saveForm no-unsafe-argument value This rule is powerful enough to deeply inspect nested data structures within function arguments. Therefore, you can be confident that passing objects as function arguments will never contain untyped data. // ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. }); The best way to fix the error is to use TypeScript’s or a validation library such as or . For instance, let's write the function that narrows the precise type of parsed data. type narrowing Zod Superstruct parseFormValues // ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues); Note that it is allowed to pass the type as an argument to a function that accepts , as there are no safety concerns associated with doing so. any unknown Writing data validation functions can be a tedious task, especially when dealing with large amounts of data. Therefore, it is worth considering the use of a data validation library. For instance, with Zod, the code would look like this: // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues); no-unsafe-assignment The rule searches for variable assignments in which a value has the type. Such assignments can mislead the compiler into thinking that a variable has a certain type, while the data may actually have a different type. no-unsafe-assignment any Consider the previous example of JSON parsing: // ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value Thanks to the rule, we can catch the type even before passing elsewhere. The fixing strategy remains the same: We can use type narrowing to provide a specific type to the variable's value. no-unsafe-assignment any formValues // ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues no-unsafe-member-access and no-unsafe-call These two rules trigger much less frequently. However, based on my experience, they are really helpful when you are trying to use poorly typed third-party dependencies. The rule prevents us from accessing object properties if a variable has the type, since it may be or . no-unsafe-member-access any null undefined The rule prevents us from calling a variable with the type as a function, as it may not be a function. no-unsafe-call any Let's imagine that we have a poorly typed third-party library called : untyped-auth // ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value. The linter highlights two issues: Calling the function can be unsafe, as we may forget to pass important arguments to the function. authenticate Reading the property from the object is unsafe, as it will be if authentication fails. name userInfo null The best way to fix these errors is to consider using a library with a strongly typed API. But if this is not an option, you can yourself. An example with the fixed library types would look like this: augment the library types // ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); } no-unsafe-return The rule helps to not accidentally return the type from a function that should return something more specific. Such cases can mislead the compiler into thinking that a returned value has a certain type, while the data may actually have a different type. no-unsafe-return any For instance, suppose we have a function that parses JSON and returns an object with two properties. // ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null The function may lead to runtime errors in any part of the program where it is used, since the parsed value is not checked. The rule prevents such runtime issues. parseForm no-unsafe-return Fixing this is easy by adding validation to ensure that the parsed JSON matches the expected type. Let's use the Zod library this time: // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); } A Note About Performance Using type-checked rules comes with a performance penalty for ESLint since it must invoke TypeScript's compiler to infer all the types. This slowdown is mainly noticeable when running the linter in pre-commit hooks and in CI, but it is not noticeable when working in an IDE. The type checking is performed once on IDE startup and then updates the types as you change the code. It is worth noting that just inferring the types works faster than the usual invocation of the compiler. For example, on our most recent project with about 1.5 million lines of TypeScript code, type checking through takes about 11 minutes, while the additional time required for ESLint's type-aware rules to bootstrap is only about 2 minutes. tsc tsc For our team, the additional safety provided by using type-aware static analysis rules is worth the tradeoff. On smaller projects, this decision is even easier to make. Conclusion Controlling the use of in TypeScript projects is crucial for achieving optimal type safety and code quality. By utilizing the plugin, developers can identify and eliminate any occurrences of the type in their codebase, resulting in a more robust and maintainable codebase. any typescript-eslint any By using type-aware eslint rules, any appearance of the keyword in our codebase will be a deliberate decision rather than a mistake or oversight. This approach safeguards us from using in our own code, as well as in the standard library and third-party dependencies. any any Overall, a type-aware linter allows us to achieve a level of type safety similar to that of statically typed programming languages such as Java, Go, Rust, and others. This greatly simplifies the development and maintenance of large projects. I hope you have learned something new from this article. Thank you for reading! Useful Links Library: typescript-eslint Article: Key Considerations in tsconfig Article: Improving Standard Library Types Docs: Type Narrowing in TypeScript Docs: Type Inference in TypeScript Library: Zod Library: Superstruct