UNPKG

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