Skip to content

Commit

Permalink
feat(eslint-plugin): [no-type-alias] support tuples (#775)
Browse files Browse the repository at this point in the history
  • Loading branch information
ankeetmaini authored and JamesHenry committed Aug 19, 2019
1 parent 14c6f80 commit c68e033
Show file tree
Hide file tree
Showing 4 changed files with 408 additions and 76 deletions.
76 changes: 76 additions & 0 deletions packages/eslint-plugin/docs/rules/no-type-alias.md
Expand Up @@ -86,6 +86,7 @@ or more of the following you may pass an object with the options set as follows:
- `allowCallbacks` set to `"always"` will allow you to use type aliases with callbacks (Defaults to `"never"`)
- `allowLiterals` set to `"always"` will allow you to use type aliases with literal objects (Defaults to `"never"`)
- `allowMappedTypes` set to `"always"` will allow you to use type aliases as mapping tools (Defaults to `"never"`)
- `allowTupleTypes` set to `"always"` will allow you to use type aliases with tuples (Defaults to `"never"`)

### allowAliases

Expand Down Expand Up @@ -453,6 +454,81 @@ type Foo<T, U> = { readonly [P in keyof T]: T[P] } &
type Foo<T, U> = { [P in keyof T]?: T[P] } & { [P in keyof U]?: U[P] };
```

### allowTupleTypes

This applies to tuple types (`type Foo = [number]`).

The setting accepts the following options:

- `"always"` or `"never"` to active or deactivate the feature.
- `"in-unions"`, allows tuples in union statements, e.g. `type Foo = [string] | [string, string];`
- `"in-intersections"`, allows tuples in intersection statements, e.g. `type Foo = [string] & [string, string];`
- `"in-unions-and-intersections"`, allows tuples in union and/or intersection statements.

Examples of **correct** code for the `{ "allowTupleTypes": "always" }` options:

```ts
type Foo = [number];

type Foo = [number] | [number, number];

type Foo = [number] & [number, number];

type Foo = [number] | [number, number] & [string, string];
```

Examples of **incorrect** code for the `{ "allowTupleTypes": "in-unions" }` option:

```ts
type Foo = [number];

type Foo = [number] & [number, number];

type Foo = [string] & [number];
```

Examples of **correct** code for the `{ "allowTupleTypes": "in-unions" }` option:

```ts
type Foo = [number] | [number, number];

type Foo = [string] | [number];
```

Examples of **incorrect** code for the `{ "allowTupleTypes": "in-intersections" }` option:

```ts
type Foo = [number];

type Foo = [number] | [number, number];

type Foo = [string] | [number];
```

Examples of **correct** code for the `{ "allowTupleTypes": "in-intersections" }` option:

```ts
type Foo = [number] & [number, number];

type Foo = [string] & [number];
```

Examples of **incorrect** code for the `{ "allowTupleTypes": "in-unions-and-intersections" }` option:

```ts
type Foo = [number];

type Foo = [string];
```

Examples of **correct** code for the `{ "allowLiterals": "in-unions-and-intersections" }` option:

```ts
type Foo = [number] & [number, number];

type Foo = [string] | [number];
```

## When Not To Use It

When you can't express some shape with an interface or you need to use a union, tuple type,
Expand Down
145 changes: 70 additions & 75 deletions packages/eslint-plugin/src/rules/no-type-alias.ts
Expand Up @@ -4,27 +4,27 @@ import {
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

type Values =
| 'always'
| 'never'
| 'in-unions'
| 'in-intersections'
| 'in-unions-and-intersections';
const enumValues: Values[] = [
'always',
'never',
'in-unions',
'in-intersections',
'in-unions-and-intersections',
];

type Options = [
{
allowAliases?:
| 'always'
| 'never'
| 'in-unions'
| 'in-intersections'
| 'in-unions-and-intersections';
allowAliases?: Values;
allowCallbacks?: 'always' | 'never';
allowLiterals?:
| 'always'
| 'never'
| 'in-unions'
| 'in-intersections'
| 'in-unions-and-intersections';
allowMappedTypes?:
| 'always'
| 'never'
| 'in-unions'
| 'in-intersections'
| 'in-unions-and-intersections';
allowLiterals?: Values;
allowMappedTypes?: Values;
allowTupleTypes?: Values;
},
];
type MessageIds = 'noTypeAlias' | 'noCompositionAlias';
Expand Down Expand Up @@ -57,34 +57,19 @@ export default util.createRule<Options, MessageIds>({
type: 'object',
properties: {
allowAliases: {
enum: [
'always',
'never',
'in-unions',
'in-intersections',
'in-unions-and-intersections',
],
enum: enumValues,
},
allowCallbacks: {
enum: ['always', 'never'],
},
allowLiterals: {
enum: [
'always',
'never',
'in-unions',
'in-intersections',
'in-unions-and-intersections',
],
enum: enumValues,
},
allowMappedTypes: {
enum: [
'always',
'never',
'in-unions',
'in-intersections',
'in-unions-and-intersections',
],
enum: enumValues,
},
allowTupleTypes: {
enum: enumValues,
},
},
additionalProperties: false,
Expand All @@ -97,11 +82,20 @@ export default util.createRule<Options, MessageIds>({
allowCallbacks: 'never',
allowLiterals: 'never',
allowMappedTypes: 'never',
allowTupleTypes: 'never',
},
],
create(
context,
[{ allowAliases, allowCallbacks, allowLiterals, allowMappedTypes }],
[
{
allowAliases,
allowCallbacks,
allowLiterals,
allowMappedTypes,
allowTupleTypes,
},
],
) {
const unions = ['always', 'in-unions', 'in-unions-and-intersections'];
const intersections = [
Expand Down Expand Up @@ -180,6 +174,36 @@ export default util.createRule<Options, MessageIds>({
});
}

const isValidTupleType = (type: TypeWithLabel) => {
if (type.node.type === AST_NODE_TYPES.TSTupleType) {
return true;
}
if (type.node.type === AST_NODE_TYPES.TSTypeOperator) {
if (
['keyof', 'readonly'].includes(type.node.operator) &&
type.node.typeAnnotation &&
type.node.typeAnnotation.type === AST_NODE_TYPES.TSTupleType
) {
return true;
}
}
return false;
};

const checkAndReport = (
optionValue: Values,
isTopLevel: boolean,
type: TypeWithLabel,
label: string,
) => {
if (
optionValue === 'never' ||
!isSupportedComposition(isTopLevel, type.compositionType, optionValue)
) {
reportError(type.node, type.compositionType, isTopLevel, label);
}
};

/**
* Validates the node looking for aliases, callbacks and literals.
* @param node the node to be validated.
Expand All @@ -198,48 +222,19 @@ export default util.createRule<Options, MessageIds>({
}
} else if (type.node.type === AST_NODE_TYPES.TSTypeLiteral) {
// literal object type
if (
allowLiterals === 'never' ||
!isSupportedComposition(
isTopLevel,
type.compositionType,
allowLiterals!,
)
) {
reportError(type.node, type.compositionType, isTopLevel, 'Literals');
}
checkAndReport(allowLiterals!, isTopLevel, type, 'Literals');
} else if (type.node.type === AST_NODE_TYPES.TSMappedType) {
// mapped type
if (
allowMappedTypes === 'never' ||
!isSupportedComposition(
isTopLevel,
type.compositionType,
allowMappedTypes!,
)
) {
reportError(
type.node,
type.compositionType,
isTopLevel,
'Mapped types',
);
}
checkAndReport(allowMappedTypes!, isTopLevel, type, 'Mapped types');
} else if (isValidTupleType(type)) {
// tuple types
checkAndReport(allowTupleTypes!, isTopLevel, type, 'Tuple Types');
} else if (
type.node.type.endsWith('Keyword') ||
aliasTypes.has(type.node.type)
) {
// alias / keyword
if (
allowAliases === 'never' ||
!isSupportedComposition(
isTopLevel,
type.compositionType,
allowAliases!,
)
) {
reportError(type.node, type.compositionType, isTopLevel, 'Aliases');
}
checkAndReport(allowAliases!, isTopLevel, type, 'Aliases');
} else {
// unhandled type - shouldn't happen
reportError(type.node, type.compositionType, isTopLevel, 'Unhandled');
Expand Down

0 comments on commit c68e033

Please sign in to comment.