tslint-immutable
Advanced tools
Comparing version 1.0.0 to 2.0.0
{ | ||
"name": "tslint-immutable", | ||
"version": "1.0.0", | ||
"description": "TSLint rules to disable all mutation in TypeScript.", | ||
"version": "2.0.0", | ||
"description": "TSLint rules to disable mutation in TypeScript.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "mocha tests/*.ts --require ts-node/register", | ||
"test": "yarn run tsc && mocha tests/*.ts --require ts-node/register", | ||
"tsc": "tsc", | ||
"build": "tsc -p tsconfig.prod.json", | ||
"publish:major": "rm -rf rules && npm run build && node scripts/publish.js major", | ||
"publish:minor": "rm -rf rules && npm run build && node scripts/publish.js minor", | ||
"publish:patch": "rm -rf rules && npm run build && node scripts/publish.js patch" | ||
"publish:major": "npm run build && node scripts/publish.js major", | ||
"publish:minor": "npm run build && node scripts/publish.js minor", | ||
"publish:patch": "npm run build && node scripts/publish.js patch" | ||
}, | ||
@@ -29,3 +30,3 @@ "repository": { | ||
"@types/chai": "^3.4.34", | ||
"@types/mocha": "^2.2.32", | ||
"@types/mocha": "^2.2.33", | ||
"@types/node": "^6.0.46", | ||
@@ -35,6 +36,6 @@ "chai": "^3.5.0", | ||
"shelljs": "^0.7.5", | ||
"ts-node": "^1.6.1", | ||
"tslint": "^3.15.1", | ||
"ts-node": "^1.7.2", | ||
"tslint": "^4.0.2", | ||
"typescript": "^2.0.6" | ||
} | ||
} |
171
README.md
# tslint-immutable | ||
This repo contains a [TSLint](https://github.com/palantir/tslint) version of the [eslint-plugin-immutable | ||
](https://github.com/jhusain/eslint-plugin-immutable). | ||
[TSLint](https://palantir.github.io/tslint/) rules to disable mutation in TypeScript. | ||
This goal of these TSLint rules are to disable all mutation in TypeScript. Please see the [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable) version for the rationale beind this. | ||
> NOTE: tslint-immutable >= 2.0.0 is compatible with tslint >= 4.0.0. For older versions that is compatible with older tslint versions please check the [v1](https://github.com/jonaskello/tslint-immutable/tree/v1) branch. | ||
## Background | ||
In some projects it is important to not mutatable 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. | ||
I 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). | ||
Then 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. | ||
This 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. | ||
## Installing | ||
@@ -13,14 +22,112 @@ | ||
## TSLint Rules | ||
There following rules are availavle: | ||
### no-let | ||
See eslint version for [documentation](https://github.com/jhusain/eslint-plugin-immutable/blob/master/README.md#no-let). | ||
In addition to immutable rules this project also contains a few rules for enforcing a functional style and a few other rules. The following rules are available: | ||
### no-this | ||
See eslint version for [documentation](https://github.com/jhusain/eslint-plugin-immutable/blob/master/README.md#no-this). | ||
### Immutability rules | ||
### no-mutation | ||
See eslint version for [documentation](https://github.com/jhusain/eslint-plugin-immutable/blob/master/README.md#no-mutation). | ||
#### readonly-interface | ||
### no-expression-statement | ||
This rule enforces having the `readonly` modifier on all interface members. | ||
You might think that prohibiting the use of `let` and `var` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`. | ||
```TypeScript | ||
interface Point { x: number, y: number } | ||
const point: Point = { x: 23, y: 44 }; | ||
point.x = 99; // This is legal | ||
``` | ||
This is why the `readonly-interface` rule exists. This rule prevents you from assigning a value to the result of a member expression. | ||
```TypeScript | ||
interface Point { readonly x: number, readonly y: number } | ||
const point: Point = { x: 23, y: 44 }; | ||
point.x = 99; // <- No object mutation allowed. | ||
``` | ||
This rule is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However this rule has **no run-time cost**, and is enforced at **compile time**. A good alternative to object mutation is to use the 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. | ||
```TypeScript | ||
interface Point { readonly x: number, readonly y: number } | ||
const point: Point = { x: 23, y: 44 }; | ||
const transformedPoint = { ...point, x: 99 }; | ||
``` | ||
#### readonly-array | ||
This rule enforces use of `ReadonlyArray<T>` instead of `Array<T>` or `T[]`. | ||
Even if an array is declared with `const` it is still possible to mutate the contents of the array. | ||
```TypeScript | ||
interface Point { readonly x: number, readonly y: number } | ||
const points: Array<Point> = [{ x: 23, y: 44 }]; | ||
points.push({ x: 1, y: 2 }); // This is legal | ||
``` | ||
Using the readonly-array rule will stop this mutation: | ||
```TypeScript | ||
interface Point { readonly x: number, readonly y: number } | ||
const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }]; | ||
points.push({ x: 1, y: 2 }); // Unresolved method push() | ||
``` | ||
#### no-let | ||
This rule should be combined with tslint's built-in `no-var` rule to enforce that all variables are declared as `const`. | ||
There'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. | ||
```TypeScript | ||
let x = 5; // <- Unexpected let or var, use const. | ||
``` | ||
What 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). | ||
```TypeScript | ||
const SearchResults = | ||
({ results }) => | ||
<ul>{ | ||
results.map(result => <li>result</li>) // <- Who needs let? | ||
}</ul>; | ||
``` | ||
#### no-this, no-class, no-new | ||
Thanks 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. | ||
```TypeScript | ||
const Message = React.createClass({ | ||
render: function() { | ||
return <div>{ this.props.message }</div>; // <- no this allowed | ||
} | ||
}) | ||
``` | ||
Instead 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: | ||
```TypeScript | ||
const Message = ({message}) => <div>{ message }</div>; | ||
``` | ||
What 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. | ||
```TypeScript | ||
import { pure, onlyUpdateForKeys } from 'recompose'; | ||
const Message = ({message}) => <div>{ message }</div>; | ||
// Optimized version of same component, using shallow comparison of props | ||
// Same effect as React's PureRenderMixin | ||
const OptimizedMessage = pure(Message); | ||
// Even more optimized: only updates if specific prop keys have changed | ||
const HyperOptimizedMessage = onlyUpdateForKeys(['message'], Message); | ||
``` | ||
### Functional style rules | ||
#### no-mixed-interface | ||
Mixing 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. | ||
#### no-expression-statement | ||
When 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. | ||
@@ -33,17 +140,35 @@ | ||
This rule checks that the value of an expression is assigned to a variable. However it will still be possible to perform mutations, eg. this rule will not catch these expressions: | ||
This rule checks that the value of an expression is assigned to a variable and thus helps promote side-effect free (pure) functions. | ||
```TypeScript | ||
const namesWithBar = names.concat([' bar ']) // ok, bound to another variable | ||
return namesWithBar // ok, returned | ||
const newSize = names.push('baz') // maybe `no-unused-vars` can catch this | ||
const newObject = Object.assign(oldObject, { foo: 'bar' }) // tough luck | ||
``` | ||
### Other rules | ||
## Supplementary TSLint Rules to Enable | ||
#### import-containment | ||
The rules in this package alone can not eliminate mutation in your TypeScript programs. To go the distance I suggest you also enable the following built-in TSLint rules: | ||
ECMAScript modules does not have a concept of a library that can span multiple files and share internal members. If you have a set of files that forms an library, and they need to be able to call each other internally without exposing members to other files outside the library set, this rule can be useful. | ||
* no-var-keyword (self-explanatory) | ||
#### no-arguments | ||
Disallows use of the `arguments` keyword. | ||
#### no-label | ||
Disallows the use of labels, and indirectly also `goto`. | ||
#### no-semicolon-interface | ||
Ensures that interfaces only use commas as separator instead semicolor. | ||
```TypeScript | ||
// This is NOT ok. | ||
inferface Foo { | ||
bar: string; | ||
zoo(): number; | ||
} | ||
// This is ok. | ||
inferface Foo { | ||
bar: string, | ||
zoo(): number, | ||
} | ||
``` | ||
## Sample Configuration File | ||
@@ -65,1 +190,5 @@ | ||
``` | ||
## Prior work | ||
This work was originally inspired by [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable). |
@@ -8,7 +8,7 @@ "use strict"; | ||
var ts = require("typescript"); | ||
var Lint = require("tslint/lib/lint"); | ||
var Lint = require("tslint"); | ||
var Rule = (function (_super) { | ||
__extends(Rule, _super); | ||
function Rule() { | ||
_super.apply(this, arguments); | ||
return _super.apply(this, arguments) || this; | ||
} | ||
@@ -19,5 +19,5 @@ Rule.prototype.apply = function (sourceFile) { | ||
}; | ||
Rule.FAILURE_STRING = "Using expressions to cause side-effects not allowed."; | ||
return Rule; | ||
}(Lint.Rules.AbstractRule)); | ||
Rule.FAILURE_STRING = "Using expressions to cause side-effects not allowed."; | ||
exports.Rule = Rule; | ||
@@ -27,3 +27,3 @@ var NoExpressionStatementWalker = (function (_super) { | ||
function NoExpressionStatementWalker() { | ||
_super.apply(this, arguments); | ||
return _super.apply(this, arguments) || this; | ||
} | ||
@@ -30,0 +30,0 @@ NoExpressionStatementWalker.prototype.visitNode = function (node) { |
@@ -8,7 +8,7 @@ "use strict"; | ||
var ts = require("typescript"); | ||
var Lint = require("tslint/lib/lint"); | ||
var Lint = require("tslint"); | ||
var Rule = (function (_super) { | ||
__extends(Rule, _super); | ||
function Rule() { | ||
_super.apply(this, arguments); | ||
return _super.apply(this, arguments) || this; | ||
} | ||
@@ -19,5 +19,5 @@ Rule.prototype.apply = function (sourceFile) { | ||
}; | ||
Rule.FAILURE_STRING = "Unexpected let, use const."; | ||
return Rule; | ||
}(Lint.Rules.AbstractRule)); | ||
Rule.FAILURE_STRING = "Unexpected let, use const."; | ||
exports.Rule = Rule; | ||
@@ -27,3 +27,3 @@ var NoLetKeywordWalker = (function (_super) { | ||
function NoLetKeywordWalker() { | ||
_super.apply(this, arguments); | ||
return _super.apply(this, arguments) || this; | ||
} | ||
@@ -30,0 +30,0 @@ NoLetKeywordWalker.prototype.visitVariableStatement = function (node) { |
@@ -0,0 +0,0 @@ "use strict"; |
@@ -8,7 +8,7 @@ "use strict"; | ||
var ts = require("typescript"); | ||
var Lint = require("tslint/lib/lint"); | ||
var Lint = require("tslint"); | ||
var Rule = (function (_super) { | ||
__extends(Rule, _super); | ||
function Rule() { | ||
_super.apply(this, arguments); | ||
return _super.apply(this, arguments) || this; | ||
} | ||
@@ -19,5 +19,5 @@ Rule.prototype.apply = function (sourceFile) { | ||
}; | ||
Rule.FAILURE_STRING = "Unexpected this, use functions not classes."; | ||
return Rule; | ||
}(Lint.Rules.AbstractRule)); | ||
Rule.FAILURE_STRING = "Unexpected this, use functions not classes."; | ||
exports.Rule = Rule; | ||
@@ -27,3 +27,3 @@ var NoThisWalker = (function (_super) { | ||
function NoThisWalker() { | ||
_super.apply(this, arguments); | ||
return _super.apply(this, arguments) || this; | ||
} | ||
@@ -30,0 +30,0 @@ NoThisWalker.prototype.visitNode = function (node) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
75277
19
606
192
0
1