UNPKG

23.9 kBMarkdownView Raw
1# tslint-immutable
2
3[![npm version][version-image]][version-url]
4[![travis build][travis-image]][travis-url]
5[![Coverage Status][coveralls-image]][coveralls-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* [Recommended built-in rules](#recommended-built-in-rules)
56
57## Immutability rules
58
59### readonly-keyword
60
61This rule enforces use of the `readonly` modifier. The `readonly` modifier can appear on property signatures in interfaces, property declarations in classes, and index signatures.
62
63Below is some information about the `readonly` modifier and the benefits of using it:
64
65You might think that using `const` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`.
66
67```typescript
68interface Point {
69 x: number;
70 y: number;
71}
72const point: Point = { x: 23, y: 44 };
73point.x = 99; // This is legal
74```
75
76This is why the `readonly` modifier exists. It prevents you from assigning a value to the result of a member expression.
77
78```typescript
79interface Point {
80 readonly x: number;
81 readonly y: number;
82}
83const point: Point = { x: 23, y: 44 };
84point.x = 99; // <- No object mutation allowed.
85```
86
87This 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:
88
89```typescript
90interface Point {
91 readonly x: number;
92 readonly y: number;
93}
94const point: Point = { x: 23, y: 44 };
95const transformedPoint = { ...point, x: 99 };
96```
97
98Note 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:
99
100```typescript
101let { [action.id]: deletedItem, ...rest } = state;
102```
103
104The `readonly` modifier also works on indexers:
105
106```typescript
107const foo: { readonly [key: string]: number } = { a: 1, b: 2 };
108foo["a"] = 3; // Error: Index signature only permits reading
109```
110
111#### Has Fixer
112
113Yes
114
115#### Options
116
117* [ignore-local](#using-the-ignore-local-option)
118* [ignore-class](#using-the-ignore-class-option)
119* [ignore-interface](#using-the-ignore-interface-option)
120* [ignore-prefix](#using-the-ignore-prefix-option)
121
122#### Example config
123
124```javascript
125"readonly-keyword": true
126```
127
128```javascript
129"readonly-keyword": [true, "ignore-local"]
130```
131
132```javascript
133"readonly-keyword": [true, "ignore-local", {"ignore-prefix": "mutable"}]
134```
135
136### readonly-array
137
138This rule enforces use of `ReadonlyArray<T>` instead of `Array<T>` or `T[]`.
139
140Below is some information about the `ReadonlyArray<T>` type and the benefits of using it:
141
142Even if an array is declared with `const` it is still possible to mutate the contents of the array.
143
144```typescript
145interface Point {
146 readonly x: number;
147 readonly y: number;
148}
149const points: Array<Point> = [{ x: 23, y: 44 }];
150points.push({ x: 1, y: 2 }); // This is legal
151```
152
153Using the `ReadonlyArray<T>` type will stop this mutation:
154
155```typescript
156interface Point {
157 readonly x: number;
158 readonly y: number;
159}
160const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }];
161points.push({ x: 1, y: 2 }); // Unresolved method push()
162```
163
164#### Has Fixer
165
166Yes
167
168#### Options
169
170* [ignore-local](#using-the-ignore-local-option)
171* [ignore-prefix](#using-the-ignore-prefix-option)
172* [ignore-return-type](#using-the-ignore-return-type-option)
173* [ignore-rest-parameters](#using-the-ignore-rest-parameters-option)
174
175#### Example config
176
177```javascript
178"readonly-array": true
179```
180
181```javascript
182"readonly-array": [true, "ignore-local"]
183```
184
185```javascript
186"readonly-array": [true, "ignore-local", {"ignore-prefix": "mutable"}]
187```
188
189### no-let
190
191This rule should be combined with tslint's built-in `no-var-keyword` rule to enforce that all variables are declared as `const`.
192
193There'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.
194
195```typescript
196let x = 5; // <- Unexpected let or var, use const.
197```
198
199What 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).
200
201```typescript
202const SearchResults = ({ results }) => (
203 <ul>
204 {results.map(result => <li>result</li>) // <- Who needs let?
205 }
206 </ul>
207);
208```
209
210#### Has Fixer
211
212Yes
213
214#### Options
215
216* [ignore-local](#using-the-ignore-local-option)
217* [ignore-prefix](#using-the-ignore-prefix-option)
218
219#### Example config
220
221```javascript
222"no-let": true
223```
224
225```javascript
226"no-let": [true, "ignore-local"]
227```
228
229```javascript
230"no-let": [true, "ignore-local", {"ignore-prefix": "mutable"}]
231```
232
233### no-array-mutation
234
235[![Type Info Required][type-info-badge]][type-info-url]
236
237This 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)).
238
239```typescript
240const x = [0, 1, 2];
241
242x[0] = 4; // <- Mutating an array is not allowed.
243x.length = 1; // <- Mutating an array is not allowed.
244x.push(3); // <- Mutating an array is not allowed.
245```
246
247#### Has Fixer
248
249No
250
251#### Options
252
253* [ignore-prefix](#using-the-ignore-prefix-option)
254* [ignore-mutation-following-accessor](#using-the-ignore-mutation-following-accessor-option-with-no-array-mutation)
255
256#### Example config
257
258```javascript
259"no-array-mutation": true
260```
261
262```javascript
263"no-array-mutation": [true, {"ignore-prefix": "mutable"}]
264```
265
266```javascript
267"no-array-mutation": [true, "ignore-mutation-following-accessor"]
268```
269
270### no-object-mutation
271
272This 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).
273
274```typescript
275const x = { a: 1 };
276
277x.foo = "bar"; // <- Modifying properties of existing object not allowed.
278x.a += 1; // <- Modifying properties of existing object not allowed.
279delete x.a; // <- Modifying properties of existing object not allowed.
280```
281
282#### Has Fixer
283
284No
285
286#### Options
287
288* [ignore-prefix](#using-the-ignore-prefix-option)
289
290#### Example config
291
292```javascript
293"no-object-mutation": true
294```
295
296```javascript
297"no-object-mutation": [true, {"ignore-prefix": "mutable"}]
298```
299
300### no-method-signature
301
302There are two ways function members can be declared in an interface or type alias:
303
304```typescript
305interface Zoo {
306 foo(): string; // MethodSignature, cannot have readonly modifier
307 readonly bar: () => string; // PropertySignature
308}
309```
310
311The `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.
312
313### no-delete
314
315The delete operator allows for mutating objects by deleting keys. This rule disallows any delete expressions.
316
317```typescript
318delete object.property; // Unexpected delete, objects should be considered immutable.
319```
320
321As an alternative the spread operator can be used to delete a key in an object (as noted [here](https://stackoverflow.com/a/35676025/2761797)):
322
323```typescript
324const { [action.id]: deletedItem, ...rest } = state;
325```
326
327## Functional style rules
328
329### no-this, no-class
330
331Thanks 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.
332
333```typescript
334const Message = React.createClass({
335 render: function() {
336 return <div>{this.props.message}</div>; // <- no this allowed
337 }
338});
339```
340
341Instead 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:
342
343```typescript
344const Message = ({ message }) => <div>{message}</div>;
345```
346
347What 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.
348
349```typescript
350import { pure, onlyUpdateForKeys } from "recompose";
351
352const Message = ({ message }) => <div>{message}</div>;
353
354// Optimized version of same component, using shallow comparison of props
355// Same effect as React's PureRenderMixin
356const OptimizedMessage = pure(Message);
357
358// Even more optimized: only updates if specific prop keys have changed
359const HyperOptimizedMessage = onlyUpdateForKeys(["message"], Message);
360```
361
362### no-mixed-interface
363
364Mixing 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.
365
366### no-expression-statement
367
368When 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.
369
370```typescript
371array.push(1);
372alert("Hello world!");
373```
374
375This rule checks that the value of an expression is assigned to a variable and thus helps promote side-effect free (pure) functions.
376
377#### Options
378
379* [ignore-prefix](#using-the-ignore-prefix-option-with-no-expression-statement)
380
381#### Example config
382
383```javascript
384"no-expression-statement": true
385```
386
387```javascript
388"no-expression-statement": [true, {"ignore-prefix": "console."}]
389```
390
391```javascript
392"no-expression-statement": [true, {"ignore-prefix": ["console.log", "console.error"]}]
393```
394
395### no-if-statement
396
397If 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.
398
399```typescript
400let x;
401if (i === 1) {
402 x = 2;
403} else {
404 x = 3;
405}
406```
407
408Instead 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:
409
410```typescript
411const x = i === 1 ? 2 : 3;
412```
413
414For 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).
415
416### no-loop-statement
417
418In 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`.
419
420```typescript
421const numbers = [1, 2, 3];
422const double = [];
423for (let i = 0; i < numbers.length; i++) {
424 double[i] = numbers[i] * 2;
425}
426```
427
428Instead consider using `map` or `reduce`:
429
430```typescript
431const numbers = [1, 2, 3];
432const double = numbers.map(n => n * 2);
433```
434
435For 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).
436
437### no-throw
438
439Exceptions are not part of functional programming.
440
441```typescript
442throw new Error("Something went wrong."); // Unexpected throw, throwing exceptions is not functional.
443```
444
445As an alternative a function should return an error:
446
447```typescript
448function divide(x: number, y: number): number | Error {
449 return y === 0 ? new Error("Cannot divide by zero.") : x / y;
450}
451```
452
453Or in the case of an async function, a rejected promise should be returned:
454
455```typescript
456async function divide(x: Promise<number>, y: Promise<number>): Promise<number> {
457 const [xv, yv] = await Promise.all([x, y]);
458
459 return yv === 0
460 ? Promise.reject(new Error("Cannot divide by zero."))
461 : xv / yv;
462}
463```
464
465### no-try
466
467Try statements are not part of functional programming. See [no-throw](#no-throw) for more information.
468
469## Options
470
471### Using the `ignore-local` option
472
473> If a tree falls in the woods, does it make a sound?
474> If a pure function mutates some local data in order to produce an immutable return value, is that ok?
475
476The 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.
477
478Note that using this option can lead to more imperative code in functions so use with care!
479
480### Using the `ignore-class` option
481
482Doesn't check for `readonly` in classes.
483
484### Using the `ignore-interface` option
485
486Doesn't check for `readonly` in interfaces.
487
488### Using the `ignore-rest-parameters` option
489
490Doesn't check for `ReadonlyArray` for function rest parameters.
491
492### Using the `ignore-return-type` option
493
494Doesn't check the return type of functions.
495
496### Using the `ignore-prefix` option
497
498Some 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:
499
500```reason
501type person = {
502 name: string,
503 mutable age: int
504};
505```
506
507Typescript 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:
508
509```typescript
510type person = {
511 readonly name: string;
512 mutableAge: number; // This is OK with ignore-prefix = "mutable"
513};
514```
515
516Yes, variable names like `mutableAge` are ugly, but then again mutation is an ugly business :-).
517
518### Using the `ignore-prefix` option with `no-expression-statement`
519
520Expression 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.
521
522One such prefix could be `console.`, which would cover both these cases:
523
524```typescript
525const doSomething(arg:string) => {
526 if (arg) {
527 console.log("Argument is", arg);
528 } else {
529 console.warn("Argument is empty!");
530 }
531 return `Hello ${arg}`;
532}
533```
534
535### Using the `ignore-mutation-following-accessor` option with `no-array-mutation`
536
537This option allows for the use of array mutator methods to be chained to an array accessor method (methods that modify the original array are known as [mutator methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Mutator_methods) (eg. `sort`) and methods that return a copy are known as [accessor methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Accessor_methods) (eg. `slice` and `concat`)).
538
539For example, an array can be immutably sorted with a single line like so:
540
541```typescript
542const sorted = ["foo", "bar"].slice().sort((a, b) => a.localeCompare(b)); // This is OK with ignore-mutation-following-accessor
543```
544
545## Recommended built-in rules
546
547### [no-var-keyword](https://palantir.github.io/tslint/rules/no-var-keyword/)
548
549Without this rule, it is still possible to create `var` variables that are mutable.
550
551### [no-parameter-reassignment](https://palantir.github.io/tslint/rules/no-parameter-reassignment/)
552
553Without this rule, function parameters are mutable.
554
555### [typedef](https://palantir.github.io/tslint/rules/typedef/) with call-signature option
556
557For 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):
558
559```javascript
560function foo() {
561 return [1, 2, 3];
562}
563```
564
565To avoid this situation you can enable the built in typedef rule like this:
566
567`"typedef": [true, "call-signature"]`
568
569Now the above function is forced to declare the return type becomes this and will be detected.
570
571## Sample Configuration File
572
573Here's a sample TSLint configuration file (tslint.json) that activates all the rules:
574
575```javascript
576{
577 "extends": ["tslint-immutable"],
578 "rules": {
579
580 // Recommended built-in rules
581 "no-var-keyword": true,
582 "no-parameter-reassignment": true,
583 "typedef": [true, "call-signature"],
584
585 // Immutability rules
586 "readonly-keyword": true,
587 "readonly-array": true,
588 "no-let": true,
589 "no-object-mutation": true,
590 "no-delete": true,
591 "no-method-signature": true,
592
593 // Functional style rules
594 "no-this": true,
595 "no-class": true,
596 "no-mixed-interface": true,
597 "no-expression-statement": true,
598 "no-if-statement": true
599
600 }
601}
602```
603
604It is also possible to enable all the rules in tslint-immutable by extending `tslint-immutable/all` like this:
605
606```javascript
607{
608 "extends": ["tslint-immutable/all"]
609}
610```
611
612## How to contribute
613
614For 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/).
615
616## How to develop
617
618To execute the tests first run `yarn build` and then run `yarn test`.
619
620While 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`.
621
622Please 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.
623
624In order to know which AST nodes are created for a snippet of typescript code you can use [ast explorer](https://astexplorer.net/).
625
626To release a new package version run `yarn publish:patch`, `yarn publish:minor`, or `yarn publish:major`.
627
628## Prior work
629
630This work was originally inspired by [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable).
631
632[version-image]: https://img.shields.io/npm/v/tslint-immutable.svg?style=flat
633[version-url]: https://www.npmjs.com/package/tslint-immutable
634[travis-image]: https://travis-ci.org/jonaskello/tslint-immutable.svg?branch=master&style=flat
635[travis-url]: https://travis-ci.org/jonaskello/tslint-immutable
636[coveralls-image]: https://coveralls.io/repos/github/jonaskello/tslint-immutable/badge.svg?branch=master
637[coveralls-url]: https://coveralls.io/github/jonaskello/tslint-immutable?branch=master
638[license-image]: https://img.shields.io/github/license/jonaskello/tslint-immutable.svg?style=flat
639[license-url]: https://opensource.org/licenses/MIT
640[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat
641[prettier-url]: https://github.com/prettier/prettier
642[type-info-badge]: https://img.shields.io/badge/type_info-requried-d51313.svg?style=flat
643[type-info-url]: https://palantir.github.io/tslint/usage/type-checking