UNPKG

24.6 kBMarkdownView Raw
1# tslint-immutable
2
3[![npm version][version-image]][version-url]
4[![travis build][travis-image]][travis-url]
5[![Coverage Status][codecov-image]][codecov-url]
6[![code style: prettier][prettier-image]][prettier-url]
7[![MIT license][license-image]][license-url]
8
9[TSLint](https://palantir.github.io/tslint/) rules to disable mutation in TypeScript.
10
11## Background
12
13In some applications it is important to not mutate any data, for example when using Redux to store state in a React application. Moreover immutable data structures has a lot of advantages in general so I want to use them everywhere in my applications.
14
15I originally used [immutablejs](https://github.com/facebook/immutable-js/) for this purpose. It is a really nice library but I found it had some drawbacks. Specifically when debugging it was hard to see the structure, creating JSON was not straightforward, and passing parameters to other libraries required converting to regular mutable arrays and objects. The [seamless-immutable](https://github.com/rtfeldman/seamless-immutable) project seems to have the same conclusions and they use regular objects and arrays and check for immutability at run-time. This solves all the aformentioned drawbacks but introduces a new drawback of only being enforced at run-time. (Altough you loose the structural sharing feature of immutablejs with this solution so you would have to consider if that is something you need).
16
17Then typescript 2.0 came along and introduced [readonly](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#read-only-properties-and-index-signatures) options for properties, indexers and arrays. This enables us to use regular object and arrays and have the immutability enfored at compile time instead of run-time. Now the only drawback is that there is nothing enforcing the use of readonly in typescript.
18
19This can be solved by using linting rules. So the aim of this project is to leverage the type system in typescript to enforce immutability at compile-time while still using regular objects and arrays.
20
21## Installing
22
23`npm install tslint-immutable --save-dev`
24
25See the [example](#sample-configuration-file) tslint.json file for configuration.
26
27## Compability
28
29* tslint-immutable 5.x.x requires typescript >=2.8, node >=6, and tslint 5.x.x.
30* tslint-immutable 3.x.x requires tslint 5.x.x.
31* tslint-immutable 2.x.x requires tslint 4.x.x.
32* tslint-immutable 1.x.x requires tslint 3.x.x.
33
34## TSLint Rules
35
36In addition to immutable rules this project also contains a few rules for enforcing a functional style of programming. The following rules are available:
37
38* [Immutability rules](#immutability-rules)
39 * [readonly-keyword](#readonly-keyword)
40 * [readonly-array](#readonly-array)
41 * [no-let](#no-let)
42 * [no-array-mutation](#no-array-mutation)
43 * [no-object-mutation](#no-object-mutation)
44 * [no-method-signature](#no-method-signature)
45 * [no-delete](#no-delete)
46* [Functional style rules](#functional-style-rules)
47 * [no-this](#no-this-no-class)
48 * [no-class](#no-this-no-class)
49 * [no-mixed-interface](#no-mixed-interface)
50 * [no-expression-statement](#no-expression-statement)
51 * [no-if-statement](#no-if-statement)
52 * [no-loop-statement](#no-loop-statement)
53 * [no-throw](#no-throw)
54 * [no-try](#no-try)
55 * [no-reject](#no-reject)
56* [Recommended built-in rules](#recommended-built-in-rules)
57
58## Immutability rules
59
60### readonly-keyword
61
62This rule enforces use of the `readonly` modifier. The `readonly` modifier can appear on property signatures in interfaces, property declarations in classes, and index signatures.
63
64Below is some information about the `readonly` modifier and the benefits of using it:
65
66You might think that using `const` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`.
67
68```typescript
69interface Point {
70 x: number;
71 y: number;
72}
73const point: Point = { x: 23, y: 44 };
74point.x = 99; // This is legal
75```
76
77This is why the `readonly` modifier exists. It prevents you from assigning a value to the result of a member expression.
78
79```typescript
80interface Point {
81 readonly x: number;
82 readonly y: number;
83}
84const point: Point = { x: 23, y: 44 };
85point.x = 99; // <- No object mutation allowed.
86```
87
88This is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However the `readonly` modifier has **no run-time cost**, and is enforced at **compile time**. A good alternative to object mutation is to use the ES2016 object spread [syntax](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#object-spread-and-rest) that was added in typescript 2.1:
89
90```typescript
91interface Point {
92 readonly x: number;
93 readonly y: number;
94}
95const point: Point = { x: 23, y: 44 };
96const transformedPoint = { ...point, x: 99 };
97```
98
99Note that you can also use object spread when destructuring to [delete keys](http://stackoverflow.com/questions/35342355/remove-data-from-nested-objects-without-mutating/35676025#35676025) in an object:
100
101```typescript
102let { [action.id]: deletedItem, ...rest } = state;
103```
104
105The `readonly` modifier also works on indexers:
106
107```typescript
108const foo: { readonly [key: string]: number } = { a: 1, b: 2 };
109foo["a"] = 3; // Error: Index signature only permits reading
110```
111
112#### Has Fixer
113
114Yes
115
116#### Options
117
118* [ignore-local](#using-the-ignore-local-option)
119* [ignore-class](#using-the-ignore-class-option)
120* [ignore-interface](#using-the-ignore-interface-option)
121* [ignore-prefix](#using-the-ignore-prefix-option)
122
123#### Example config
124
125```javascript
126"readonly-keyword": true
127```
128
129```javascript
130"readonly-keyword": [true, "ignore-local"]
131```
132
133```javascript
134"readonly-keyword": [true, "ignore-local", {"ignore-prefix": "mutable"}]
135```
136
137### readonly-array
138
139This rule enforces use of `ReadonlyArray<T>` instead of `Array<T>` or `T[]`.
140
141Below is some information about the `ReadonlyArray<T>` type and the benefits of using it:
142
143Even if an array is declared with `const` it is still possible to mutate the contents of the array.
144
145```typescript
146interface Point {
147 readonly x: number;
148 readonly y: number;
149}
150const points: Array<Point> = [{ x: 23, y: 44 }];
151points.push({ x: 1, y: 2 }); // This is legal
152```
153
154Using the `ReadonlyArray<T>` type will stop this mutation:
155
156```typescript
157interface Point {
158 readonly x: number;
159 readonly y: number;
160}
161const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }];
162points.push({ x: 1, y: 2 }); // Unresolved method push()
163```
164
165#### Has Fixer
166
167Yes
168
169#### Options
170
171* [ignore-local](#using-the-ignore-local-option)
172* [ignore-prefix](#using-the-ignore-prefix-option)
173* [ignore-return-type](#using-the-ignore-return-type-option)
174* [ignore-rest-parameters](#using-the-ignore-rest-parameters-option)
175
176#### Example config
177
178```javascript
179"readonly-array": true
180```
181
182```javascript
183"readonly-array": [true, "ignore-local"]
184```
185
186```javascript
187"readonly-array": [true, "ignore-local", {"ignore-prefix": "mutable"}]
188```
189
190### no-let
191
192This rule should be combined with tslint's built-in `no-var-keyword` rule to enforce that all variables are declared as `const`.
193
194There's no reason to use `let` in a Redux/React application, because all your state is managed by either Redux or React. Use `const` instead, and avoid state bugs altogether.
195
196```typescript
197let x = 5; // <- Unexpected let or var, use const.
198```
199
200What about `for` loops? Loops can be replaced with the Array methods like `map`, `filter`, and so on. If you find the built-in JS Array methods lacking, use [ramda](http://ramdajs.com/), or [lodash-fp](https://github.com/lodash/lodash/wiki/FP-Guide).
201
202```typescript
203const SearchResults = ({ results }) => (
204 <ul>
205 {results.map(result => <li>result</li>) // <- Who needs let?
206 }
207 </ul>
208);
209```
210
211#### Has Fixer
212
213Yes
214
215#### Options
216
217* [ignore-local](#using-the-ignore-local-option)
218* [ignore-prefix](#using-the-ignore-prefix-option)
219
220#### Example config
221
222```javascript
223"no-let": true
224```
225
226```javascript
227"no-let": [true, "ignore-local"]
228```
229
230```javascript
231"no-let": [true, "ignore-local", {"ignore-prefix": "mutable"}]
232```
233
234### no-array-mutation
235
236[![Type Info Required][type-info-badge]][type-info-url]
237
238This rule prohibits mutating an array via assignment to or deletion of their elements/properties. This rule enforces array immutability without the use of `ReadonlyArray<T>` (as apposed to [readonly-array](#readonly-array)).
239
240```typescript
241const x = [0, 1, 2];
242
243x[0] = 4; // <- Mutating an array is not allowed.
244x.length = 1; // <- Mutating an array is not allowed.
245x.push(3); // <- Mutating an array is not allowed.
246```
247
248#### Has Fixer
249
250No
251
252#### Options
253
254* [ignore-prefix](#using-the-ignore-prefix-option)
255* [ignore-new-array](#using-the-ignore-new-array-option-with-no-array-mutation)
256* ~~ignore-mutation-following-accessor~~ - _deprecated in favor of [ignore-new-array](#using-the-ignore-new-array-option-with-no-array-mutation)_
257
258#### Example config
259
260```javascript
261"no-array-mutation": true
262```
263
264```javascript
265"no-array-mutation": [true, {"ignore-prefix": "mutable"}]
266```
267
268```javascript
269"no-array-mutation": [true, "ignore-new-array"]
270```
271
272### no-object-mutation
273
274This rule prohibits syntax that mutates existing objects via assignment to or deletion of their properties. While requiring the `readonly` modifier forces declared types to be immutable, it won't stop assignment into or modification of untyped objects or external types declared under different rules. Forbidding forms like `a.b = 'c'` is one way to plug this hole. Inspired by the no-mutation rule of [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable).
275
276```typescript
277const x = { a: 1 };
278
279x.foo = "bar"; // <- Modifying properties of existing object not allowed.
280x.a += 1; // <- Modifying properties of existing object not allowed.
281delete x.a; // <- Modifying properties of existing object not allowed.
282```
283
284#### Has Fixer
285
286No
287
288#### Options
289
290* [ignore-prefix](#using-the-ignore-prefix-option)
291
292#### Example config
293
294```javascript
295"no-object-mutation": true
296```
297
298```javascript
299"no-object-mutation": [true, {"ignore-prefix": "mutable"}]
300```
301
302### no-method-signature
303
304There are two ways function members can be declared in an interface or type alias:
305
306```typescript
307interface Zoo {
308 foo(): string; // MethodSignature, cannot have readonly modifier
309 readonly bar: () => string; // PropertySignature
310}
311```
312
313The `MethodSignature` and the `PropertySignature` forms seem equivalent, but only the `PropertySignature` form can have a `readonly` modifier. Becuase of this any `MethodSignature` will be mutable. Therefore the `no-method-signature` rule disallows usage of this form and instead proposes to use the `PropertySignature` which can have a `readonly` modifier. It should be noted however that the `PropertySignature` form for declaring functions does not support overloading.
314
315### no-delete
316
317The delete operator allows for mutating objects by deleting keys. This rule disallows any delete expressions.
318
319```typescript
320delete object.property; // Unexpected delete, objects should be considered immutable.
321```
322
323As an alternative the spread operator can be used to delete a key in an object (as noted [here](https://stackoverflow.com/a/35676025/2761797)):
324
325```typescript
326const { [action.id]: deletedItem, ...rest } = state;
327```
328
329## Functional style rules
330
331### no-this, no-class
332
333Thanks to libraries like [recompose](https://github.com/acdlite/recompose) and Redux's [React Container components](http://redux.js.org/docs/basics/UsageWithReact.html), there's not much reason to build Components using `React.createClass` or ES6 classes anymore. The `no-this` rule makes this explicit.
334
335```typescript
336const Message = React.createClass({
337 render: function() {
338 return <div>{this.props.message}</div>; // <- no this allowed
339 }
340});
341```
342
343Instead of creating classes, you should use React 0.14's [Stateless Functional Components](https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.t5z2fdit6) and save yourself some keystrokes:
344
345```typescript
346const Message = ({ message }) => <div>{message}</div>;
347```
348
349What about lifecycle methods like `shouldComponentUpdate`? We can use the [recompose](https://github.com/acdlite/recompose) library to apply these optimizations to your Stateless Functional Components. The [recompose](https://github.com/acdlite/recompose) library relies on the fact that your Redux state is immutable to efficiently implement shouldComponentUpdate for you.
350
351```typescript
352import { pure, onlyUpdateForKeys } from "recompose";
353
354const Message = ({ message }) => <div>{message}</div>;
355
356// Optimized version of same component, using shallow comparison of props
357// Same effect as React's PureRenderMixin
358const OptimizedMessage = pure(Message);
359
360// Even more optimized: only updates if specific prop keys have changed
361const HyperOptimizedMessage = onlyUpdateForKeys(["message"], Message);
362```
363
364### no-mixed-interface
365
366Mixing functions and data properties in the same interface is a sign of object-orientation style. This rule enforces that an inteface only has one type of members, eg. only data properties or only functions.
367
368### no-expression-statement
369
370When you call a function and don’t use it’s return value, chances are high that it is being called for its side effect. e.g.
371
372```typescript
373array.push(1);
374alert("Hello world!");
375```
376
377This rule checks that the value of an expression is assigned to a variable and thus helps promote side-effect free (pure) functions.
378
379#### Options
380
381* [ignore-prefix](#using-the-ignore-prefix-option-with-no-expression-statement)
382
383#### Example config
384
385```javascript
386"no-expression-statement": true
387```
388
389```javascript
390"no-expression-statement": [true, {"ignore-prefix": "console."}]
391```
392
393```javascript
394"no-expression-statement": [true, {"ignore-prefix": ["console.log", "console.error"]}]
395```
396
397### no-if-statement
398
399If statements is not a good fit for functional style programming as they are not expresssions and do not return a value. This rule disallows if statements.
400
401```typescript
402let x;
403if (i === 1) {
404 x = 2;
405} else {
406 x = 3;
407}
408```
409
410Instead consider using the [tenary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) which is an expression that returns a value:
411
412```typescript
413const x = i === 1 ? 2 : 3;
414```
415
416For more background see this [blog post](https://hackernoon.com/rethinking-javascript-the-if-statement-b158a61cd6cb) and discussion in [#54](https://github.com/jonaskello/tslint-immutable/issues/54).
417
418### no-loop-statement
419
420In functional programming we want everthing to be an expression that returns a value. Loops in typescript are statements so they are not a good fit for a functional programming style. This rule disallows for loop statements, including `for`, `for...of`, `for...in`, `while`, and `do...while`.
421
422```typescript
423const numbers = [1, 2, 3];
424const double = [];
425for (let i = 0; i < numbers.length; i++) {
426 double[i] = numbers[i] * 2;
427}
428```
429
430Instead consider using `map` or `reduce`:
431
432```typescript
433const numbers = [1, 2, 3];
434const double = numbers.map(n => n * 2);
435```
436
437For more background see this [blog post](https://hackernoon.com/rethinking-javascript-death-of-the-for-loop-c431564c84a8) and discussion in [#54](https://github.com/jonaskello/tslint-immutable/issues/54).
438
439### no-throw
440
441Exceptions are not part of functional programming.
442
443```typescript
444throw new Error("Something went wrong."); // Unexpected throw, throwing exceptions is not functional.
445```
446
447As an alternative a function should return an error:
448
449```typescript
450function divide(x: number, y: number): number | Error {
451 return y === 0 ? new Error("Cannot divide by zero.") : x / y;
452}
453```
454
455Or in the case of an async function, a rejected promise should be returned.
456
457```typescript
458async function divide(x: Promise<number>, y: Promise<number>): Promise<number> {
459 const [xv, yv] = await Promise.all([x, y]);
460
461 return yv === 0
462 ? Promise.reject(new Error("Cannot divide by zero."))
463 : xv / yv;
464}
465```
466
467### no-try
468
469Try statements are not part of functional programming. See [no-throw](#no-throw) for more information.
470
471### no-reject
472
473You can view a `Promise` as a result object with built-in error (something like `{ value: number } | { error: Error }`) in which case a rejected `Promise` can be viewed as a returned result and thus fits with functional programming. You can also view a rejected promise as something similar to an exception and as such something that does not fit with functional programming. If your view is the latter you can use the `no-reject` rule to disallow rejected promises.
474
475```typescript
476async function divide(
477 x: Promise<number>,
478 y: Promise<number>
479): Promise<number | Error> {
480 const [xv, yv] = await Promise.all([x, y]);
481
482 // Rejecting the promise is not allowed so resolve to an Error instead
483
484 // return yv === 0
485 // ? Promise.reject(new Error("Cannot divide by zero."))
486 // : xv / yv;
487
488 return yv === 0 ? new Error("Cannot divide by zero.") : xv / yv;
489}
490```
491
492## Options
493
494### Using the `ignore-local` option
495
496> If a tree falls in the woods, does it make a sound?
497> If a pure function mutates some local data in order to produce an immutable return value, is that ok?
498
499The quote above is from the [clojure docs](https://clojure.org/reference/transients). In general, it is more important to enforce immutability for state that is passed in and out of functions than for local state used for internal calculations within a function. For example in Redux, the state going in and out of reducers needs to be immutable while the reducer may be allowed to mutate local state in its calculations in order to achieve higher performance. This is what the `ignore-local` option enables. With this option enabled immutability will be enforced everywhere but in local state. Function parameters and return types are not considered local state so they will still be checked.
500
501Note that using this option can lead to more imperative code in functions so use with care!
502
503### Using the `ignore-class` option
504
505Doesn't check for `readonly` in classes.
506
507### Using the `ignore-interface` option
508
509Doesn't check for `readonly` in interfaces.
510
511### Using the `ignore-rest-parameters` option
512
513Doesn't check for `ReadonlyArray` for function rest parameters.
514
515### Using the `ignore-return-type` option
516
517Doesn't check the return type of functions.
518
519### Using the `ignore-prefix` option
520
521Some languages are immutable by default but allows you to explicitly declare mutable variables. For example in [reason](https://facebook.github.io/reason/) you can declare mutable record fields like this:
522
523```reason
524type person = {
525 name: string,
526 mutable age: int
527};
528```
529
530Typescript is not immutable by default but it can be if you use this package. So in order to create an escape hatch similar to how it is done in reason the `ignore-prefix` option can be used. For example if you configure it to ignore variables with names that has the prefix "mutable" you can emulate the above example in typescript like this:
531
532```typescript
533type person = {
534 readonly name: string;
535 mutableAge: number; // This is OK with ignore-prefix = "mutable"
536};
537```
538
539Yes, variable names like `mutableAge` are ugly, but then again mutation is an ugly business :-).
540
541### Using the `ignore-prefix` option with `no-expression-statement`
542
543Expression statements typically cause side effects, however not all side effects are undesirable. One example of a helpful side effect is logging. To not get warning of every log statement, we can configure the linter to ignore well known expression statement prefixes.
544
545One such prefix could be `console.`, which would cover both these cases:
546
547```typescript
548const doSomething(arg:string) => {
549 if (arg) {
550 console.log("Argument is", arg);
551 } else {
552 console.warn("Argument is empty!");
553 }
554 return `Hello ${arg}`;
555}
556```
557
558### Using the `ignore-new-array` option with `no-array-mutation`
559
560This option allows for the use of array mutator methods to be chained to newly created arrays.
561
562For example, an array can be immutably sorted like so:
563
564```typescript
565const original = ["foo", "bar", "baz"];
566const sorted = original.slice().sort((a, b) => a.localeCompare(b)); // This is OK with ignore-new-array - note the use of the `slice` method which returns a copy of the original array.
567```
568
569## Recommended built-in rules
570
571### [no-var-keyword](https://palantir.github.io/tslint/rules/no-var-keyword/)
572
573Without this rule, it is still possible to create `var` variables that are mutable.
574
575### [no-parameter-reassignment](https://palantir.github.io/tslint/rules/no-parameter-reassignment/)
576
577Without this rule, function parameters are mutable.
578
579### [typedef](https://palantir.github.io/tslint/rules/typedef/) with call-signature option
580
581For performance reasons, tslint-immutable does not check implicit return types. So for example this function will return an mutable array but will not be detected (see [#18](https://github.com/jonaskello/tslint-immutable/issues/18) for more info):
582
583```javascript
584function foo() {
585 return [1, 2, 3];
586}
587```
588
589To avoid this situation you can enable the built in typedef rule like this:
590
591`"typedef": [true, "call-signature"]`
592
593Now the above function is forced to declare the return type becomes this and will be detected.
594
595## Sample Configuration File
596
597Here's a sample TSLint configuration file (tslint.json) that activates all the rules:
598
599```javascript
600{
601 "extends": ["tslint-immutable"],
602 "rules": {
603
604 // Recommended built-in rules
605 "no-var-keyword": true,
606 "no-parameter-reassignment": true,
607 "typedef": [true, "call-signature"],
608
609 // Immutability rules
610 "readonly-keyword": true,
611 "readonly-array": true,
612 "no-let": true,
613 "no-object-mutation": true,
614 "no-delete": true,
615 "no-method-signature": true,
616
617 // Functional style rules
618 "no-this": true,
619 "no-class": true,
620 "no-mixed-interface": true,
621 "no-expression-statement": true,
622 "no-if-statement": true
623
624 }
625}
626```
627
628It is also possible to enable all the rules in tslint-immutable by extending `tslint-immutable/all` like this:
629
630```javascript
631{
632 "extends": ["tslint-immutable/all"]
633}
634```
635
636## How to contribute
637
638For new features file an issue. For bugs, file an issue and optionally file a PR with a failing test. Tests are really easy to do, you just have to edit the `*.ts.lint` files under the test directory. Read more here about [tslint testing](https://palantir.github.io/tslint/develop/testing-rules/).
639
640## How to develop
641
642To execute the tests first run `yarn build` and then run `yarn test`.
643
644While working on the code you can run `yarn test:work`. This script also builds before running the tests. To run a subset of the tests, change the path for `yarn test:work` in `package.json`.
645
646Please review the [tslint performance tips](https://palantir.github.io/tslint/develop/custom-rules/performance-tips.html) in order to write rules that run efficiently at run-time. For example, note that using `SyntaxWalker` or any subclass thereof like `RuleWalker` is inefficient. Note that tslint requires the use of `class` as an entrypoint, but you can make a very small class that inherits from `AbstractRule` which directly calls `this.applyWithFunction` and from there you can switch to using a more functional programming style.
647
648In order to know which AST nodes are created for a snippet of typescript code you can use [ast explorer](https://astexplorer.net/).
649
650## How to publish
651
652```
653yarn version --patch
654yarn version --minor
655yarn version --major
656```
657
658## Prior work
659
660This work was originally inspired by [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable).
661
662[version-image]: https://img.shields.io/npm/v/tslint-immutable.svg?style=flat
663[version-url]: https://www.npmjs.com/package/tslint-immutable
664[travis-image]: https://travis-ci.com/jonaskello/tslint-immutable.svg?branch=master&style=flat
665[travis-url]: https://travis-ci.com/jonaskello/tslint-immutable
666[codecov-image]: https://codecov.io/gh/jonaskello/tslint-immutable/branch/master/graph/badge.svg
667[codecov-url]: https://codecov.io/gh/jonaskello/tslint-immutable
668[license-image]: https://img.shields.io/github/license/jonaskello/tslint-immutable.svg?style=flat
669[license-url]: https://opensource.org/licenses/MIT
670[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat
671[prettier-url]: https://github.com/prettier/prettier
672[type-info-badge]: https://img.shields.io/badge/type_info-required-d51313.svg?style=flat
673[type-info-url]: https://palantir.github.io/tslint/usage/type-checking