From 0e4e151909b3ec7ec7e8ff8ca029610aa18d21a0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 23 Feb 2022 18:39:15 -0500 Subject: [PATCH] feat(eslint-plugin): add `no-redundant-type-constituents` rule (#4378) --- .cspell.json | 5 +- packages/eslint-plugin/README.md | 1 + .../rules/no-redundant-type-constituents.md | 85 ++ packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../rules/no-redundant-type-constituents.ts | 457 ++++++++++ packages/eslint-plugin/src/util/misc.ts | 21 + .../no-redundant-type-constituents.test.ts | 787 ++++++++++++++++++ packages/type-utils/src/predicates.ts | 19 + 9 files changed, 1376 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-redundant-type-constituents.md create mode 100644 packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts create mode 100644 packages/eslint-plugin/tests/rules/no-redundant-type-constituents.test.ts diff --git a/.cspell.json b/.cspell.json index 898e10932518..4a8c0ad226be 100644 --- a/.cspell.json +++ b/.cspell.json @@ -71,8 +71,8 @@ "IIFE", "IIFEs", "linebreaks", - "markdownlint", "lzstring", + "markdownlint", "necroing", "nocheck", "nullish", @@ -101,14 +101,15 @@ "transpiled", "transpiles", "transpiling", - "tsvfs", "tsconfigs", "tsutils", + "tsvfs", "typedef", "typedefs", "unfixable", "unoptimized", "unprefixed", + "upsert", "Zacher" ], "overrides": [ diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 47012b81b8fc..d10d9f079907 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -136,6 +136,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-non-null-asserted-optional-chain`](./docs/rules/no-non-null-asserted-optional-chain.md) | Disallows using a non-null assertion after an optional chain expression | :white_check_mark: | | | | [`@typescript-eslint/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallows non-null assertions using the `!` postfix operator | :white_check_mark: | | | | [`@typescript-eslint/no-parameter-properties`](./docs/rules/no-parameter-properties.md) | Disallow the use of parameter properties in class constructors | | | | +| [`@typescript-eslint/no-redundant-type-constituents`](./docs/rules/no-redundant-type-constituents.md) | Disallow members of unions and intersections that do nothing or override type information | | | :thought_balloon: | | [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | | | [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :white_check_mark: | | | | [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | | diff --git a/packages/eslint-plugin/docs/rules/no-redundant-type-constituents.md b/packages/eslint-plugin/docs/rules/no-redundant-type-constituents.md new file mode 100644 index 000000000000..e427c671d3fc --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-redundant-type-constituents.md @@ -0,0 +1,85 @@ +# `no-redundant-type-constituents` + +Disallow members of unions and intersections that do nothing or override type information. + +## Rule Details + +Some types can override some other types ("constituents") in a union or intersection and/or be overridden by some other types. +TypeScript's set theory of types includes cases where a constituent type might be useless in the parent union or intersection. + +Within `|` unions: + +- `any` and `unknown` "override" all other union members +- `never` is dropped from unions in any position except when in a return type position +- primitive types such as `string` "override" any of their literal types such as `""` + +Within `&` intersections: + +- `any` and `never` "override" all other intersection members +- `unknown` is dropped from intersections +- literal types "override" any primitive types in an intersection +- literal types such as `""` "override" any of their primitive types such as `string` + +Examples of code for this rule: + + + +### ❌ Incorrect + +```ts +type UnionAny = any | 'foo'; +type UnionUnknown = unknown | 'foo'; +type UnionNever = never | 'foo'; + +type UnionBooleanLiteral = boolean | false; +type UnionNumberLiteral = number | 1; +type UnionStringLiteral = string | 'foo'; + +type IntersectionAny = any & 'foo'; +type IntersectionUnknown = string & unknown; +type IntersectionNever = string | never; + +type IntersectionBooleanLiteral = boolean & false; +type IntersectionNumberLiteral = number & 1; +type IntersectionStringLiteral = string & 'foo'; +``` + +### ✅ Correct + +```ts +type UnionAny = any; +type UnionUnknown = unknown; +type UnionNever = never; + +type UnionBooleanLiteral = boolean; +type UnionNumberLiteral = number; +type UnionStringLiteral = string; + +type IntersectionAny = any; +type IntersectionUnknown = string; +type IntersectionNever = string; + +type IntersectionBooleanLiteral = false; +type IntersectionNumberLiteral = 1; +type IntersectionStringLiteral = 'foo'; + +type ReturnUnionNever = () => string | never; +``` + +## Limitations + +This rule plays it safe and only works with bottom types, top types, and comparing literal types to primitive types. +It also does not provide an auto-fixer just yet. + +## Further Reading + +- [Union Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) +- [Intersection Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) +- [Bottom Types](https://en.wikipedia.org/wiki/Bottom_type) +- [Top Types](https://en.wikipedia.org/wiki/Top_type) + +## Attributes + +- [ ] ✅ Recommended +- [ ] 🔧 Fixable +- [x] 💭 Requires type information diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 538be53ce583..c052ce4eb37d 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -88,6 +88,7 @@ export = { '@typescript-eslint/no-parameter-properties': 'error', 'no-redeclare': 'off', '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'error', '@typescript-eslint/no-require-imports': 'error', 'no-restricted-imports': 'off', '@typescript-eslint/no-restricted-imports': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 83e76f0a23d0..96251c3c22f0 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -60,6 +60,7 @@ import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chai import noNonNullAssertion from './no-non-null-assertion'; import noParameterProperties from './no-parameter-properties'; import noRedeclare from './no-redeclare'; +import noRedundantTypeConstituents from './no-redundant-type-constituents'; import noRequireImports from './no-require-imports'; import noRestrictedImports from './no-restricted-imports'; import noShadow from './no-shadow'; @@ -183,6 +184,7 @@ export default { 'no-non-null-assertion': noNonNullAssertion, 'no-parameter-properties': noParameterProperties, 'no-redeclare': noRedeclare, + 'no-redundant-type-constituents': noRedundantTypeConstituents, 'no-require-imports': noRequireImports, 'no-restricted-imports': noRestrictedImports, 'no-shadow': noShadow, diff --git a/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts b/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts new file mode 100644 index 000000000000..1ee8c3c49594 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts @@ -0,0 +1,457 @@ +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; +import * as util from '../util'; + +const literalToPrimitiveTypeFlags = { + [ts.TypeFlags.BigIntLiteral]: ts.TypeFlags.BigInt, + [ts.TypeFlags.BooleanLiteral]: ts.TypeFlags.Boolean, + [ts.TypeFlags.NumberLiteral]: ts.TypeFlags.Number, + [ts.TypeFlags.StringLiteral]: ts.TypeFlags.String, + [ts.TypeFlags.TemplateLiteral]: ts.TypeFlags.String, +} as const; + +const literalTypeFlags = [ + ts.TypeFlags.BigIntLiteral, + ts.TypeFlags.BooleanLiteral, + ts.TypeFlags.NumberLiteral, + ts.TypeFlags.StringLiteral, + ts.TypeFlags.TemplateLiteral, +] as const; + +const primitiveTypeFlags = [ + ts.TypeFlags.BigInt, + ts.TypeFlags.Boolean, + ts.TypeFlags.Number, + ts.TypeFlags.String, +] as const; + +const primitiveTypeFlagNames = { + [ts.TypeFlags.BigInt]: 'bigint', + [ts.TypeFlags.Boolean]: 'boolean', + [ts.TypeFlags.Number]: 'number', + [ts.TypeFlags.String]: 'string', +} as const; + +const primitiveTypeFlagTypes = { + bigint: ts.TypeFlags.BigIntLiteral, + boolean: ts.TypeFlags.BooleanLiteral, + number: ts.TypeFlags.NumberLiteral, + string: ts.TypeFlags.StringLiteral, +} as const; + +const keywordNodeTypesToTsTypes = new Map([ + [TSESTree.AST_NODE_TYPES.TSAnyKeyword, ts.TypeFlags.Any], + [TSESTree.AST_NODE_TYPES.TSBigIntKeyword, ts.TypeFlags.BigInt], + [TSESTree.AST_NODE_TYPES.TSBooleanKeyword, ts.TypeFlags.Boolean], + [TSESTree.AST_NODE_TYPES.TSNeverKeyword, ts.TypeFlags.Never], + [TSESTree.AST_NODE_TYPES.TSUnknownKeyword, ts.TypeFlags.Unknown], + [TSESTree.AST_NODE_TYPES.TSNumberKeyword, ts.TypeFlags.Number], + [TSESTree.AST_NODE_TYPES.TSStringKeyword, ts.TypeFlags.String], +]); + +type PrimitiveTypeFlag = typeof primitiveTypeFlags[number]; + +interface TypeFlagsWithName { + typeFlags: ts.TypeFlags; + typeName: string; +} + +interface TypeNodeWithValue { + literalValue: unknown; + typeNode: TSESTree.TypeNode; +} + +function addToMapGroup( + map: Map, + key: Key, + value: Value, +): void { + const existing = map.get(key); + + if (existing) { + existing.push(value); + } else { + map.set(key, [value]); + } +} + +function describeLiteralType(type: ts.Type): string { + if (type.isStringLiteral()) { + return JSON.stringify(type.value); + } + + if (type.isLiteral()) { + return type.value.toString(); + } + + if (util.isTypeAnyType(type)) { + return 'any'; + } + + if (util.isTypeNeverType(type)) { + return 'never'; + } + + if (util.isTypeUnknownType(type)) { + return 'unknown'; + } + + if (util.isTypeTemplateLiteralType(type)) { + return 'template literal type'; + } + + if (util.isTypeBigIntLiteralType(type)) { + return `${type.value.negative ? '-' : ''}${type.value.base10Value}n`; + } + + if (tsutils.isBooleanLiteralType(type, true)) { + return 'true'; + } + + if (tsutils.isBooleanLiteralType(type, false)) { + return 'false'; + } + + return 'literal type'; +} + +function describeLiteralTypeNode(typeNode: TSESTree.TypeNode): string { + switch (typeNode.type) { + case AST_NODE_TYPES.TSAnyKeyword: + return 'any'; + case AST_NODE_TYPES.TSBooleanKeyword: + return 'boolean'; + case AST_NODE_TYPES.TSNeverKeyword: + return 'never'; + case AST_NODE_TYPES.TSNumberKeyword: + return 'number'; + case AST_NODE_TYPES.TSStringKeyword: + return 'string'; + case AST_NODE_TYPES.TSUnknownKeyword: + return 'unknown'; + case AST_NODE_TYPES.TSLiteralType: + switch (typeNode.literal.type) { + case TSESTree.AST_NODE_TYPES.Literal: + switch (typeof typeNode.literal.value) { + case 'bigint': + return `${typeNode.literal.value < 0 ? '-' : ''}${ + typeNode.literal.value + }n`; + case 'string': + return JSON.stringify(typeNode.literal.value); + default: + return `${typeNode.literal.value}`; + } + case TSESTree.AST_NODE_TYPES.TemplateLiteral: + return 'template literal type'; + } + } + + return 'literal type'; +} + +function isNodeInsideReturnType(node: TSESTree.TSUnionType): boolean { + return !!( + node.parent?.type === AST_NODE_TYPES.TSTypeAnnotation && + node.parent.parent && + (util.isFunctionType(node.parent.parent) || + util.isFunction(node.parent.parent)) + ); +} + +/** + * @remarks TypeScript stores boolean types as the union false | true, always. + */ +function unionTypePartsUnlessBoolean(type: ts.Type): ts.Type[] { + return type.isUnion() && + type.types.length === 2 && + tsutils.isBooleanLiteralType(type.types[0], false) && + tsutils.isBooleanLiteralType(type.types[1], true) + ? [type] + : tsutils.unionTypeParts(type); +} + +export default util.createRule({ + name: 'no-redundant-type-constituents', + meta: { + docs: { + description: + 'Disallow members of unions and intersections that do nothing or override type information', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + literalOverridden: `{{literal}} is overridden by {{primitive}} in this union type.`, + primitiveOverridden: `{{primitive}} is overridden by the {{literal}} in this intersection type.`, + overridden: `'{{typeName}}' is overridden by other types in this {{container}} type.`, + overrides: `'{{typeName}}' overrides all other types in this {{container}} type.`, + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + const parserServices = util.getParserServices(context); + const typesCache = new Map(); + + function getTypeNodeTypePartFlags( + typeNode: TSESTree.TypeNode, + ): TypeFlagsWithName[] { + const keywordTypeFlags = keywordNodeTypesToTsTypes.get(typeNode.type); + if (keywordTypeFlags) { + return [ + { + typeFlags: keywordTypeFlags, + typeName: describeLiteralTypeNode(typeNode), + }, + ]; + } + + if ( + typeNode.type === AST_NODE_TYPES.TSLiteralType && + typeNode.literal.type === AST_NODE_TYPES.Literal + ) { + return [ + { + typeFlags: + primitiveTypeFlagTypes[ + typeof typeNode.literal + .value as keyof typeof primitiveTypeFlagTypes + ], + typeName: describeLiteralTypeNode(typeNode), + }, + ]; + } + + if (typeNode.type === AST_NODE_TYPES.TSUnionType) { + return typeNode.types.flatMap(getTypeNodeTypePartFlags); + } + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(typeNode); + const checker = parserServices.program.getTypeChecker(); + const nodeType = checker.getTypeAtLocation(tsNode); + const typeParts = unionTypePartsUnlessBoolean(nodeType); + + return typeParts.map(typePart => ({ + typeFlags: typePart.flags, + typeName: describeLiteralType(typePart), + })); + } + + function getTypeNodeTypePartFlagsCached( + typeNode: TSESTree.TypeNode, + ): TypeFlagsWithName[] { + const existing = typesCache.get(typeNode); + if (existing) { + return existing; + } + + const created = getTypeNodeTypePartFlags(typeNode); + typesCache.set(typeNode, created); + return created; + } + + return { + 'TSIntersectionType:exit'(node: TSESTree.TSIntersectionType): void { + const seenLiteralTypes = new Map(); + const seenPrimitiveTypes = new Map< + PrimitiveTypeFlag, + TSESTree.TypeNode[] + >(); + + function checkIntersectionBottomAndTopTypes( + { typeFlags, typeName }: TypeFlagsWithName, + typeNode: TSESTree.TypeNode, + ): boolean { + for (const [messageId, checkFlag] of [ + ['overrides', ts.TypeFlags.Any], + ['overrides', ts.TypeFlags.Never], + ['overridden', ts.TypeFlags.Unknown], + ] as const) { + if (typeFlags === checkFlag) { + context.report({ + data: { + container: 'intersection', + typeName, + }, + messageId, + node: typeNode, + }); + return true; + } + } + + return false; + } + + for (const typeNode of node.types) { + const typePartFlags = getTypeNodeTypePartFlagsCached(typeNode); + + for (const typePart of typePartFlags) { + if (checkIntersectionBottomAndTopTypes(typePart, typeNode)) { + continue; + } + + for (const literalTypeFlag of literalTypeFlags) { + if (typePart.typeFlags === literalTypeFlag) { + addToMapGroup( + seenLiteralTypes, + literalToPrimitiveTypeFlags[literalTypeFlag], + typePart.typeName, + ); + break; + } + } + + for (const primitiveTypeFlag of primitiveTypeFlags) { + if (typePart.typeFlags === primitiveTypeFlag) { + addToMapGroup(seenPrimitiveTypes, primitiveTypeFlag, typeNode); + } + } + } + } + + // For each primitive type of all the seen primitive types, + // if there was a literal type seen that overrides it, + // report each of the primitive type's type nodes + for (const [primitiveTypeFlag, typeNodes] of seenPrimitiveTypes) { + const matchedLiteralTypes = seenLiteralTypes.get(primitiveTypeFlag); + if (matchedLiteralTypes) { + for (const typeNode of typeNodes) { + context.report({ + data: { + literal: matchedLiteralTypes.join(' | '), + primitive: primitiveTypeFlagNames[primitiveTypeFlag], + }, + messageId: 'primitiveOverridden', + node: typeNode, + }); + } + } + } + }, + 'TSUnionType:exit'(node: TSESTree.TSUnionType): void { + const seenLiteralTypes = new Map< + PrimitiveTypeFlag, + TypeNodeWithValue[] + >(); + const seenPrimitiveTypes = new Set(); + + function checkUnionBottomAndTopTypes( + { typeFlags, typeName }: TypeFlagsWithName, + typeNode: TSESTree.TypeNode, + ): boolean { + for (const checkFlag of [ + ts.TypeFlags.Any, + ts.TypeFlags.Unknown, + ] as const) { + if (typeFlags === checkFlag) { + context.report({ + data: { + container: 'union', + typeName, + }, + messageId: 'overrides', + node: typeNode, + }); + return true; + } + } + + if ( + typeFlags === ts.TypeFlags.Never && + !isNodeInsideReturnType(node) + ) { + context.report({ + data: { + container: 'union', + typeName: 'never', + }, + messageId: 'overridden', + node: typeNode, + }); + return true; + } + + return false; + } + + for (const typeNode of node.types) { + const typePartFlags = getTypeNodeTypePartFlagsCached(typeNode); + + for (const typePart of typePartFlags) { + if (checkUnionBottomAndTopTypes(typePart, typeNode)) { + continue; + } + + for (const literalTypeFlag of literalTypeFlags) { + if (typePart.typeFlags === literalTypeFlag) { + addToMapGroup( + seenLiteralTypes, + literalToPrimitiveTypeFlags[literalTypeFlag], + { + literalValue: typePart.typeName, + typeNode, + }, + ); + break; + } + } + + for (const primitiveTypeFlag of primitiveTypeFlags) { + if ((typePart.typeFlags & primitiveTypeFlag) !== 0) { + seenPrimitiveTypes.add(primitiveTypeFlag); + } + } + } + } + + interface TypeFlagWithText { + literalValue: unknown; + primitiveTypeFlag: PrimitiveTypeFlag; + } + + const overriddenTypeNodes = new Map< + TSESTree.TypeNode, + TypeFlagWithText[] + >(); + + // For each primitive type of all the seen literal types, + // if there was a primitive type seen that overrides it, + // upsert the literal text and primitive type under the backing type node + for (const [primitiveTypeFlag, typeNodesWithText] of seenLiteralTypes) { + if (seenPrimitiveTypes.has(primitiveTypeFlag)) { + for (const { literalValue, typeNode } of typeNodesWithText) { + addToMapGroup(overriddenTypeNodes, typeNode, { + literalValue, + primitiveTypeFlag, + }); + } + } + } + + // For each type node that had at least one overridden literal, + // group those literals by their primitive type, + // then report each primitive type with all its literals + for (const [typeNode, typeFlagsWithText] of overriddenTypeNodes) { + const grouped = util.arrayGroupByToMap( + typeFlagsWithText, + pair => pair.primitiveTypeFlag, + ); + + for (const [primitiveTypeFlag, pairs] of grouped) { + context.report({ + data: { + literal: pairs.map(pair => pair.literalValue).join(' | '), + primitive: primitiveTypeFlagNames[primitiveTypeFlag], + }, + messageId: 'literalOverridden', + node: typeNode, + }); + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index eb62a029b154..8479feb728dd 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -19,6 +19,26 @@ function upperCaseFirst(str: string): string { return str[0].toUpperCase() + str.slice(1); } +function arrayGroupByToMap( + array: T[], + getKey: (item: T) => Key, +): Map { + const groups = new Map(); + + for (const item of array) { + const key = getKey(item); + const existing = groups.get(key); + + if (existing) { + existing.push(item); + } else { + groups.set(key, [item]); + } + } + + return groups; +} + /** Return true if both parameters are equal. */ type Equal = (a: T, b: T) => boolean; @@ -148,6 +168,7 @@ function formatWordList(words: string[]): string { } export { + arrayGroupByToMap, arraysAreEqual, Equal, ExcludeKeys, diff --git a/packages/eslint-plugin/tests/rules/no-redundant-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-redundant-type-constituents.test.ts new file mode 100644 index 000000000000..4994459278f7 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-redundant-type-constituents.test.ts @@ -0,0 +1,787 @@ +import rule from '../../src/rules/no-redundant-type-constituents'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2021, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-redundant-type-constituents', rule, { + valid: [ + ` + type T = any; + type U = T; + `, + ` + type T = never; + type U = T; + `, + ` + type T = 1 | 2; + type U = T | 3; + type V = U; + `, + 'type T = () => never;', + 'type T = () => never | string;', + ` + type B = never; + type T = () => B | string; + `, + ` + type B = string; + type T = () => B | never; + `, + 'type T = () => string | never;', + 'type T = { (): string | never };', + ` + function _(): string | never { + return ''; + } + `, + ` + const _ = (): string | never => { + return ''; + }; + `, + ` + type B = string; + type T = { (): B | never }; + `, + 'type T = { new (): string | never };', + ` + type B = never; + type T = { new (): string | B }; + `, + ` + type B = unknown; + type T = B; + `, + 'type T = bigint;', + ` + type B = bigint; + type T = B; + `, + 'type T = 1n | 2n;', + ` + type B = 1n; + type T = B | 2n; + `, + 'type T = boolean;', + ` + type B = boolean; + type T = B; + `, + 'type T = false | true;', + ` + type B = false; + type T = B | true; + `, + ` + type B = true; + type T = B | false; + `, + 'type T = number;', + ` + type B = number; + type T = B; + `, + 'type T = 1 | 2;', + ` + type B = 1; + type T = B | 2; + `, + 'type T = 1 | false;', + ` + type B = 1; + type T = B | false; + `, + 'type T = string;', + ` + type B = string; + type T = B; + `, + "type T = 'a' | 'b';", + ` + type B = 'b'; + type T = 'a' | B; + `, + ` + type B = 'a'; + type T = B | 'b'; + `, + 'type T = bigint | null;', + ` + type B = bigint; + type T = B | null; + `, + 'type T = boolean | null;', + ` + type B = boolean; + type T = B | null; + `, + 'type T = number | null;', + ` + type B = number; + type T = B | null; + `, + 'type T = string | null;', + ` + type B = string; + type T = B | null; + `, + 'type T = bigint & null;', + ` + type B = bigint; + type T = B & null; + `, + 'type T = boolean & null;', + ` + type B = boolean; + type T = B & null; + `, + 'type T = number & null;', + ` + type B = number; + type T = B & null; + `, + 'type T = string & null;', + ` + type B = string; + type T = B & null; + `, + 'type T = `${string}` & null;', + ` + type B = \`\${string}\`; + type T = B & null; + `, + ], + + invalid: [ + { + code: 'type T = number | any;', + errors: [ + { + column: 19, + data: { + container: 'union', + typeName: 'any', + }, + messageId: 'overrides', + }, + ], + }, + { + code: ` + type B = number; + type T = B | any; + `, + errors: [ + { + column: 22, + data: { + container: 'union', + typeName: 'any', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = any | number;', + errors: [ + { + column: 10, + data: { + container: 'union', + typeName: 'any', + }, + messageId: 'overrides', + }, + ], + }, + { + code: ` + type B = any; + type T = B | number; + `, + errors: [ + { + column: 18, + data: { + container: 'union', + typeName: 'any', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = number | never;', + errors: [ + { + column: 19, + data: { + container: 'union', + typeName: 'never', + }, + messageId: 'overridden', + }, + ], + }, + { + code: ` + type B = number; + type T = B | never; + `, + errors: [ + { + column: 22, + data: { + container: 'union', + typeName: 'never', + }, + messageId: 'overridden', + }, + ], + }, + { + code: ` + type B = never; + type T = B | number; + `, + errors: [ + { + column: 18, + data: { + container: 'union', + typeName: 'never', + }, + messageId: 'overridden', + }, + ], + }, + { + code: 'type T = never | number;', + errors: [ + { + column: 10, + data: { + container: 'union', + typeName: 'never', + }, + messageId: 'overridden', + }, + ], + }, + { + code: 'type T = number | unknown;', + errors: [ + { + column: 19, + data: { + container: 'union', + typeName: 'unknown', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = unknown | number;', + errors: [ + { + column: 10, + data: { + container: 'union', + typeName: 'unknown', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = number | 0;', + errors: [ + { + column: 19, + data: { + literal: '0', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = number | (0 | 1);', + errors: [ + { + column: 20, + data: { + literal: '0 | 1', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = (0 | 0) | number;', + errors: [ + { + column: 11, + data: { + literal: '0 | 0', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: ` + type B = 0 | 1; + type T = (2 | B) | number; + `, + errors: [ + { + column: 19, + data: { + literal: '2 | 0 | 1', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = (0 | (1 | 2)) | number;', + errors: [ + { + column: 11, + data: { + literal: '0 | 1 | 2', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = (0 | 1) | number;', + errors: [ + { + column: 11, + data: { + literal: '0 | 1', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = (0 | (0 | 1)) | number;', + errors: [ + { + column: 11, + data: { + literal: '0 | 0 | 1', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: "type T = (2 | 'other' | 3) | number;", + errors: [ + { + column: 11, + data: { + literal: '2 | 3', + primitive: 'number', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: "type T = '' | string;", + errors: [ + { + column: 10, + data: { + literal: '""', + primitive: 'string', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: ` + type B = 'b'; + type T = B | string; + `, + errors: [ + { + column: 18, + data: { + literal: '"b"', + primitive: 'string', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = `a${number}c` | string;', + errors: [ + { + column: 10, + data: { + literal: 'template literal type', + primitive: 'string', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: ` + type B = \`a\${number}c\`; + type T = B | string; + `, + errors: [ + { + column: 18, + data: { + literal: 'template literal type', + primitive: 'string', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = `${number}` | string;', + errors: [ + { + column: 10, + data: { + literal: 'template literal type', + primitive: 'string', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = 0n | bigint;', + errors: [ + { + column: 10, + data: { + literal: '0n', + primitive: 'bigint', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = -1n | bigint;', + errors: [ + { + column: 10, + data: { + literal: '-1n', + primitive: 'bigint', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = (-1n | 1n) | bigint;', + errors: [ + { + column: 11, + data: { + literal: '-1n | 1n', + primitive: 'bigint', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: ` + type B = boolean; + type T = B | false; + `, + errors: [ + { + column: 22, + data: { + literal: 'false', + primitive: 'boolean', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = false | boolean;', + errors: [ + { + column: 10, + data: { + literal: 'false', + primitive: 'boolean', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = true | boolean;', + errors: [ + { + column: 10, + data: { + literal: 'true', + primitive: 'boolean', + }, + messageId: 'literalOverridden', + }, + ], + }, + { + code: 'type T = false & boolean;', + errors: [ + { + column: 18, + data: { + literal: 'false', + primitive: 'boolean', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: ` + type B = false; + type T = B & boolean; + `, + errors: [ + { + column: 22, + data: { + literal: 'false', + primitive: 'boolean', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: ` + type B = true; + type T = B & boolean; + `, + errors: [ + { + column: 22, + data: { + literal: 'true', + primitive: 'boolean', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: 'type T = true & boolean;', + errors: [ + { + column: 17, + data: { + literal: 'true', + primitive: 'boolean', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: 'type T = number & any;', + errors: [ + { + column: 19, + data: { + container: 'intersection', + typeName: 'any', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = any & number;', + errors: [ + { + column: 10, + data: { + container: 'intersection', + typeName: 'any', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = number & never;', + errors: [ + { + column: 19, + data: { + container: 'intersection', + typeName: 'never', + }, + messageId: 'overrides', + }, + ], + }, + { + code: ` + type B = never; + type T = B & number; + `, + errors: [ + { + column: 18, + data: { + container: 'intersection', + typeName: 'never', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = never & number;', + errors: [ + { + column: 10, + data: { + container: 'intersection', + typeName: 'never', + }, + messageId: 'overrides', + }, + ], + }, + { + code: 'type T = number & unknown;', + errors: [ + { + column: 19, + data: { + container: 'intersection', + typeName: 'unknown', + }, + messageId: 'overridden', + }, + ], + }, + { + code: 'type T = unknown & number;', + errors: [ + { + column: 10, + data: { + container: 'intersection', + typeName: 'unknown', + }, + messageId: 'overridden', + }, + ], + }, + { + code: 'type T = number & 0;', + errors: [ + { + column: 10, + data: { + literal: '0', + primitive: 'number', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: "type T = '' & string;", + errors: [ + { + column: 15, + data: { + literal: '""', + primitive: 'string', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: ` + type B = 0n; + type T = B & bigint; + `, + errors: [ + { + column: 22, + data: { + literal: '0n', + primitive: 'bigint', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: 'type T = 0n & bigint;', + errors: [ + { + column: 15, + data: { + literal: '0n', + primitive: 'bigint', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + { + code: 'type T = -1n & bigint;', + errors: [ + { + column: 16, + data: { + literal: '-1n', + primitive: 'bigint', + }, + messageId: 'primitiveOverridden', + }, + ], + }, + ], +}); diff --git a/packages/type-utils/src/predicates.ts b/packages/type-utils/src/predicates.ts index c3ece8aa3c7a..541d236bd7c9 100644 --- a/packages/type-utils/src/predicates.ts +++ b/packages/type-utils/src/predicates.ts @@ -47,6 +47,13 @@ export function isTypeArrayTypeOrUnionOfArrayTypes( return true; } +/** + * @returns true if the type is `never` + */ +export function isTypeNeverType(type: ts.Type): boolean { + return isTypeFlagSet(type, ts.TypeFlags.Never); +} + /** * @returns true if the type is `unknown` */ @@ -168,3 +175,15 @@ export function typeIsOrHasBaseType( return false; } + +export function isTypeBigIntLiteralType( + type: ts.Type, +): type is ts.BigIntLiteralType { + return isTypeFlagSet(type, ts.TypeFlags.BigIntLiteral); +} + +export function isTypeTemplateLiteralType( + type: ts.Type, +): type is ts.TemplateLiteralType { + return isTypeFlagSet(type, ts.TypeFlags.TemplateLiteral); +}