Skip to content

Add TypeScript Mapped Draft Type #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions __tests__/test.tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"module": "commonjs"
}
}
68 changes: 68 additions & 0 deletions __tests__/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import produce from '../src/immer';

interface State {
readonly num: number;
readonly foo?: string;
bar: string;
readonly baz: {
readonly x: number;
readonly y: number;
};
readonly arr: ReadonlyArray<{ readonly value: string }>;
readonly arr2: { readonly value: string }[];
}

const state: State = {
num: 0,
bar: 'foo',
baz: {
x: 1,
y: 2,
},
arr: [{ value: 'asdf' }],
arr2: [{ value: 'asdf' }],
};

const expectedState: State = {
num: 1,
foo: 'bar',
bar: 'foo',
baz: {
x: 2,
y: 3,
},
arr: [{ value: 'foo' }, { value: 'asf' }],
arr2: [{ value: 'foo' }, { value: 'asf' }],
};

it('can update readonly state via standard api', () => {
const newState = produce<State>(state, draft => {
draft.num++;
draft.foo = 'bar';
draft.bar = 'foo';
draft.baz.x++;
draft.baz.y++;
draft.arr[0].value = 'foo';
draft.arr.push({ value: 'asf' });
draft.arr2[0].value = 'foo';
draft.arr2.push({ value: 'asf' });
});
expect(newState).not.toBe(state);
expect(newState).toEqual(expectedState);
});

it('can update readonly state via curried api', () => {
const newState = produce<State>(draft => {
draft.num++;
draft.foo = 'bar';
draft.bar = 'foo';
draft.baz.x++;
draft.baz.y++;
draft.arr[0].value = 'foo';
draft.arr.push({ value: 'asf' });
draft.arr2[0].value = 'foo';
draft.arr2.push({ value: 'asf' });
})(state);
expect(newState).not.toBe(state);
expect(newState).toEqual(expectedState);
});
25 changes: 21 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"dist/"
],
"devDependencies": {
"@types/jest": "^22.0.0",
"@types/jest": "^22.2.3",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-jest": "^22.0.4",
Expand Down Expand Up @@ -68,7 +68,8 @@
"rollup-plugin-filesize": "^1.5.0",
"rollup-plugin-node-resolve": "^3.0.2",
"rollup-plugin-uglify": "^2.0.1",
"typescript": "^2.6.2",
"ts-jest": "^22.4.6",
"typescript": "^2.9.1",
"uglify-es": "^3.3.6",
"yarn-or-npm": "^2.0.4"
},
Expand All @@ -80,7 +81,23 @@
},
"jest": {
"transform": {
"^.+\\.jsx?$": "babel-jest"
}
"^.+\\.jsx?$": "babel-jest",
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "/__tests__/[^/]*[jt]sx?$",
"globals": {
"ts-jest": {
"enableTsDiagnostics": true,
"tsConfigFile": "__tests__/test.tsconfig.json"
}
},
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}
24 changes: 24 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,30 @@ console.log(increment(base).counter) // 1

The Immer package ships with type definitions inside the package, which should be picked up by TypeScript and Flow out of the box and without further configuration.

The TypeScript typings automatically remove `readonly` modifiers from your draft types and return a value that matches your original type. See this practical example:

```ts
import produce from 'immer';

interface State {
readonly x: number;
}

// `x` cannot be modified here
const state: State = {
x: 0;
};

const newState = produce<State>(draft => {
// `x` can be modified here
draft.x++;
});

// `newState.x` cannot be modified here
```

This ensures that the only place you can modify your state is in your produce callbacks. It even works recursively and with `ReadonlyArray`s!

## Immer on older JavaScript environments?

By default `produce` tries to use proxies for optimal performance. However, on older JavaScript engines `Proxy` is not available. For example, when running Microsoft Internet Explorer or React Native on Android. In such cases Immer will fallback to an ES5 compatible implementation which works identical, but is a bit slower.
Expand Down
34 changes: 23 additions & 11 deletions src/immer.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
// Mapped type to remove readonly modifiers from state
// Based on https://github.com/Microsoft/TypeScript/blob/d4dc67aab233f5a8834dff16531baf99b16fea78/tests/cases/conformance/types/conditional/conditionalTypes1.ts#L120-L129
export type DraftObject<T> = {
-readonly [P in keyof T]: Draft<T[P]>;
};
export interface DraftArray<T> extends Array<Draft<T>> { }
export type Draft<T> =
T extends any[] ? DraftArray<T[number]> :
T extends ReadonlyArray<any> ? DraftArray<T[number]> :
T extends object ? DraftObject<T> :
T;

/**
* Immer takes a state, and runs a function against it.
* That function can freely mutate the state, as it will create copies-on-write.
Expand All @@ -13,60 +25,60 @@
*/
export default function<S = any>(
currentState: S,
recipe: (this: S, draftState: S) => void | S
recipe: (this: Draft<S>, draftState: Draft<S>) => void | S
): S

// curried invocations with default initial state
// 0 additional arguments
export default function<S = any>(
recipe: (this: S, draftState: S) => void | S,
recipe: (this: Draft<S>, draftState: Draft<S>) => void | S,
initialState: S
): (currentState: S | undefined) => S
// 1 additional argument of type A
export default function<S = any, A = any>(
recipe: (this: S, draftState: S, a: A) => void | S,
recipe: (this: Draft<S>, draftState: Draft<S>, a: A) => void | S,
initialState: S
): (currentState: S | undefined, a: A) => S
// 2 additional arguments of types A and B
export default function<S = any, A = any, B = any>(
recipe: (this: S, draftState: S, a: A, b: B) => void | S,
recipe: (this: Draft<S>, draftState: Draft<S>, a: A, b: B) => void | S,
initialState: S
): (currentState: S | undefined, a: A, b: B) => S
// 3 additional arguments of types A, B and C
export default function<S = any, A = any, B = any, C = any>(
recipe: (this: S, draftState: S, a: A, b: B, c: C) => void | S,
recipe: (this: Draft<S>, draftState: Draft<S>, a: A, b: B, c: C) => void | S,
initialState: S
): (currentState: S | undefined, a: A, b: B, c: C) => S
// any number of additional arguments, but with loss of type safety
// this may be alleviated if "variadic kinds" makes it into Typescript:
// https://github.com/Microsoft/TypeScript/issues/5453
export default function<S = any>(
recipe: (this: S, draftState: S, ...extraArgs: any[]) => void | S,
recipe: (this: Draft<S>, draftState: Draft<S>, ...extraArgs: any[]) => void | S,
initialState: S
): (currentState: S | undefined, ...extraArgs: any[]) => S

// curried invocations without default initial state
// 0 additional arguments
export default function<S = any>(
recipe: (this: S, draftState: S) => void | S
recipe: (this: Draft<S>, draftState: Draft<S>) => void | S
): (currentState: S) => S
// 1 additional argument of type A
export default function<S = any, A = any>(
recipe: (this: S, draftState: S, a: A) => void | S
recipe: (this: Draft<S>, draftState: Draft<S>, a: A) => void | S
): (currentState: S, a: A) => S
// 2 additional arguments of types A and B
export default function<S = any, A = any, B = any>(
recipe: (this: S, draftState: S, a: A, b: B) => void | S
recipe: (this: Draft<S>, draftState: Draft<S>, a: A, b: B) => void | S
): (currentState: S, a: A, b: B) => S
// 3 additional arguments of types A, B and C
export default function<S = any, A = any, B = any, C = any>(
recipe: (this: S, draftState: S, a: A, b: B, c: C) => void | S
recipe: (this: Draft<S>, draftState: Draft<S>, a: A, b: B, c: C) => void | S
): (currentState: S, a: A, b: B, c: C) => S
// any number of additional arguments, but with loss of type safety
// this may be alleviated if "variadic kinds" makes it into Typescript:
// https://github.com/Microsoft/TypeScript/issues/5453
export default function<S = any>(
recipe: (this: S, draftState: S, ...extraArgs: any[]) => void | S
recipe: (this: Draft<S>, draftState: Draft<S>, ...extraArgs: any[]) => void | S
): (currentState: S, ...extraArgs: any[]) => S
/**
* Automatically freezes any state trees generated by immer.
Expand Down