Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [no-type-alias] support tuples #775

Merged
merged 8 commits into from Aug 19, 2019
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