Skip to content
5 changes: 5 additions & 0 deletions packages/eslint-plugin/docs/rules/ban-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import TabItem from '@theme/TabItem';
>
> See **https://typescript-eslint.io/rules/ban-types** for documentation.

:::danger Deprecated
**This rule is deprecated** and will be removed in typescript-eslint@v8.
See _**[Replacement of `ban-types`](/blog/announcing-typescript-eslint-v8-beta#replacement-of-ban-types)**_ for more details.
:::

Some built-in types have aliases, while some types are considered dangerous or harmful.
It's often a good idea to ban certain types to help with consistency and safety.

Expand Down
63 changes: 63 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
description: 'Disallow using the unsafe built-in Function type.'
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/no-unsafe-function-type** for documentation.

TypeScript's built-in `Function` type allows being called with any number of arguments and returns type `any`.
`Function` also allows classes or plain objects that happen to possess all properties of the `Function` class.
It's generally better to specify function parameters and return types with the function type syntax.

"Catch-all" function types include:

- `() => void`: a function that has no parameters and whose return is ignored
- `(...args: never) => unknown`: a "top type" for functions that can be assigned any function type, but can't be called

Examples of code for this rule:

<Tabs>
<TabItem value="❌ Incorrect">

```ts
let noParametersOrReturn: Function;
noParametersOrReturn = () => {};

let stringToNumber: Function;
stringToNumber = (text: string) => text.length;

let identity: Function;
identity = value => value;
```

</TabItem>
<TabItem value="✅ Correct">

```ts
let noParametersOrReturn: () => void;
noParametersOrReturn = () => {};

let stringToNumber: (text: string) => number;
stringToNumber = text => text.length;

let identity: <T>(value: T) => T;
identity = value => value;
```

</TabItem>
</Tabs>

## When Not To Use It

If your project is still onboarding to TypeScript, it might be difficult to fully replace all unsafe `Function` types with more precise function types.
You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule.

## Related To

- [`no-empty-object-type`](https://v8--typescript-eslint.netlify.app/rules/no-empty-object-type)
- [`no-restricted-types`](https://v8--typescript-eslint.netlify.app/rules/no-restricted-types)
- [`no-wrapper-object-types`](https://v8--typescript-eslint.netlify.app/rules/no-wrapper-object-types)
75 changes: 75 additions & 0 deletions packages/eslint-plugin/docs/rules/no-wrapper-object-types.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
description: 'Disallow using confusing built-in primitive class wrappers.'
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/no-wrapper-object-types** for documentation.

TypeScript defines several confusing pairs of types that look very similar to each other, but actually mean different things: `boolean`/`Boolean`, `number`/`Number`, `string`/`String`, `bigint`/`BigInt`, `symbol`/`Symbol`, `object`/`Object`.
In general, only the lowercase variant is appropriate to use.
Therefore, this rule enforces that you only use the lowercase variant.

JavaScript has [8 data types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures) at runtime, and these are described in TypeScript by the lowercase types `undefined`, `null`, `boolean`, `number`, `string`, `bigint`, `symbol`, and `object`.

As for the uppercase types, these are _structural types_ which describe JavaScript "wrapper" objects for each of the data types, such as [`Boolean`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean) and [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number).
Additionally, due to the quirks of structural typing, the corresponding primitives are _also_ assignable to these uppercase types, since they have the same "shape".

It is a universal best practice to work directly with the built-in primitives, like `0`, rather than objects that "look like" the corresponding primitive, like `new Number(0)`.

- Primitives have the expected value semantics with `==` and `===` equality checks, whereas their object counterparts are compared by reference.
That is to say, `"str" === "str"` but `new String("str") !== new String("str")`.
- Primitives have well-known behavior around truthiness/falsiness which is common to rely on, whereas all objects are truthy, regardless of the wrapped value (e.g. `new Boolean(false)` is truthy).
- TypeScript only allows arithmetic operations (e.g. `x - y`) to be performed on numeric primitives, not objects.

As a result, using the lowercase type names like `number` in TypeScript types instead of the uppercase names like `Number` is a better practice that describes code more accurately.

Examples of code for this rule:

<Tabs>
<TabItem value="❌ Incorrect">

```ts
let myBigInt: BigInt;
let myBoolean: Boolean;
let myNumber: Number;
let myString: String;
let mySymbol: Symbol;

let myObject: Object = 'allowed by TypeScript';
```

</TabItem>
<TabItem value="✅ Correct">

```ts
let myBigint: bigint;
let myBoolean: boolean;
let myNumber: number;
let myString: string;
let mySymbol: symbol;

let myObject: object = "Type 'string' is not assignable to type 'object'.";
```

</TabItem>
</Tabs>

## When Not To Use It

If your project is a rare one that intentionally deals with the class equivalents of primitives, it might not be worthwhile to use this rule.
You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule.

## Further Reading

- [MDN documentation on primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive)
- [MDN documentation on `string` primitives and `String` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_primitives_and_string_objects)

## Related To

- [`no-empty-object-type`](./no-empty-object-type.mdx)
- [`no-restricted-types`](https://v8--typescript-eslint.netlify.app/rules/no-restricted-types)
- [`no-unsafe-function-type`](https://v8--typescript-eslint.netlify.app/rules/no-unsafe-function-type)
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export = {
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-declaration-merging': 'error',
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-unary-minus': 'error',
Expand All @@ -118,6 +119,7 @@ export = {
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',
'@typescript-eslint/non-nullable-type-assertion-style': 'error',
'no-throw-literal': 'off',
'@typescript-eslint/only-throw-error': 'error',
Expand Down
4 changes: 4 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import noUnsafeAssignment from './no-unsafe-assignment';
import noUnsafeCall from './no-unsafe-call';
import noUnsafeDeclarationMerging from './no-unsafe-declaration-merging';
import noUnsafeEnumComparison from './no-unsafe-enum-comparison';
import noUnsafeFunctionType from './no-unsafe-function-type';
import noUnsafeMemberAccess from './no-unsafe-member-access';
import noUnsafeReturn from './no-unsafe-return';
import noUnsafeUnaryMinus from './no-unsafe-unary-minus';
Expand All @@ -103,6 +104,7 @@ import noUselessConstructor from './no-useless-constructor';
import noUselessEmptyExport from './no-useless-empty-export';
import noUselessTemplateLiterals from './no-useless-template-literals';
import noVarRequires from './no-var-requires';
import noWrapperObjectTypes from './no-wrapper-object-types';
import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style';
import objectCurlySpacing from './object-curly-spacing';
import onlyThrowError from './only-throw-error';
Expand Down Expand Up @@ -243,6 +245,7 @@ export default {
'no-unsafe-call': noUnsafeCall,
'no-unsafe-declaration-merging': noUnsafeDeclarationMerging,
'no-unsafe-enum-comparison': noUnsafeEnumComparison,
'no-unsafe-function-type': noUnsafeFunctionType,
'no-unsafe-member-access': noUnsafeMemberAccess,
'no-unsafe-return': noUnsafeReturn,
'no-unsafe-unary-minus': noUnsafeUnaryMinus,
Expand All @@ -253,6 +256,7 @@ export default {
'no-useless-empty-export': noUselessEmptyExport,
'no-useless-template-literals': noUselessTemplateLiterals,
'no-var-requires': noVarRequires,
'no-wrapper-object-types': noWrapperObjectTypes,
'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle,
'object-curly-spacing': objectCurlySpacing,
'only-throw-error': onlyThrowError,
Expand Down
49 changes: 49 additions & 0 deletions packages/eslint-plugin/src/rules/no-unsafe-function-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';

import { createRule, isReferenceToGlobalFunction } from '../util';

export default createRule({
name: 'no-unsafe-function-type',
meta: {
type: 'problem',
docs: {
description: 'Disallow using the unsafe built-in Function type',
},
fixable: 'code',
messages: {
bannedFunctionType: [
'The `Function` type accepts any function-like value.',
'Prefer explicitly defining any function parameters and return type.',
].join('\n'),
},
schema: [],
},
defaultOptions: [],
create(context) {
function checkBannedTypes(node: TSESTree.Node): void {
if (
node.type === AST_NODE_TYPES.Identifier &&
node.name === 'Function' &&
isReferenceToGlobalFunction('Function', node, context.sourceCode)
) {
context.report({
node,
messageId: 'bannedFunctionType',
});
}
}

return {
TSClassImplements(node): void {
checkBannedTypes(node.expression);
},
TSInterfaceHeritage(node): void {
checkBannedTypes(node.expression);
},
TSTypeReference(node): void {
checkBannedTypes(node.typeName);
},
};
},
});
70 changes: 70 additions & 0 deletions packages/eslint-plugin/src/rules/no-wrapper-object-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';

import { createRule, isReferenceToGlobalFunction } from '../util';

const classNames = new Set([
'BigInt',
// eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
'Boolean',
'Number',
'Object',
// eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
'String',
'Symbol',
]);

export default createRule({
name: 'no-wrapper-object-types',
meta: {
type: 'problem',
docs: {
description: 'Disallow using confusing built-in primitive class wrappers',
},
fixable: 'code',
messages: {
bannedClassType:
'Prefer using the primitive `{{preferred}}` as a type name, rather than the upper-cased `{{typeName}}`.',
},
schema: [],
},
defaultOptions: [],
create(context) {
function checkBannedTypes(
node: TSESTree.EntityName | TSESTree.Expression,
includeFix: boolean,
): void {
const typeName = node.type === AST_NODE_TYPES.Identifier && node.name;
if (
!typeName ||
!classNames.has(typeName) ||
!isReferenceToGlobalFunction(typeName, node, context.sourceCode)
) {
return;
}

const preferred = typeName.toLowerCase();

context.report({
data: { typeName, preferred },
fix: includeFix
? (fixer): TSESLint.RuleFix => fixer.replaceText(node, preferred)
: undefined,
messageId: 'bannedClassType',
node,
});
}

return {
TSClassImplements(node): void {
checkBannedTypes(node.expression, false);
},
TSInterfaceHeritage(node): void {
checkBannedTypes(node.expression, false);
},
TSTypeReference(node): void {
checkBannedTypes(node.typeName, true);
},
};
},
});
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './isNullLiteral';
export * from './isUndefinedIdentifier';
export * from './misc';
export * from './objectIterators';
export * from './scopeUtils';
export * from './types';
export * from './isAssignee';

Expand Down
15 changes: 15 additions & 0 deletions packages/eslint-plugin/src/util/scopeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { TSESTree } from '@typescript-eslint/utils';
import type { SourceCode } from '@typescript-eslint/utils/ts-eslint';

export function isReferenceToGlobalFunction(
calleeName: string,
node: TSESTree.Node,
sourceCode: SourceCode,
): boolean {
const ref = sourceCode
.getScope(node)
.references.find(ref => ref.identifier.name === calleeName);

// ensure it's the "global" version
return !ref?.resolved?.defs.length;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading