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