From 21a2f12b2ba089a58d1d25996e0dfb751d0154fa Mon Sep 17 00:00:00 2001 From: fivitti Date: Tue, 20 Aug 2019 00:58:49 +0200 Subject: [PATCH] Add mergerino type definitions (#37712) --- types/mergerino/index.d.ts | 87 ++++++++++++ types/mergerino/mergerino-tests.ts | 216 +++++++++++++++++++++++++++++ types/mergerino/tsconfig.json | 23 +++ types/mergerino/tslint.json | 3 + 4 files changed, 329 insertions(+) create mode 100644 types/mergerino/index.d.ts create mode 100644 types/mergerino/mergerino-tests.ts create mode 100644 types/mergerino/tsconfig.json create mode 100644 types/mergerino/tslint.json diff --git a/types/mergerino/index.d.ts b/types/mergerino/index.d.ts new file mode 100644 index 0000000000..f4a3431646 --- /dev/null +++ b/types/mergerino/index.d.ts @@ -0,0 +1,87 @@ +// Type definitions for mergerino 0.4 +// Project: https://github.com/fuzetsu/mergerino#readme +// Definitions by: Slawomir "Fivitti" Figiel +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 3.4 + +// TypeScript in version below 3.4 doesn't correctly support FunctionPatch. Arguments have "any" type. + +/** + * Nice side effect of flattening array arguments is that you can easily + * add conditions to your patches using nested arrays. + * + * Arrays may be nested in any depth. + */ +export interface DeepArray extends ReadonlyArray> { } + +/** + * If you want to fully remove a property from an object specify undefined as the value. + */ +export type DeletePatch = undefined; + +/** + * If you want to replace a property based on its current value, use a function. + * + * If you pass a function it will receive the current value as the first argument + * and the merge function as the second. The return value will be the replacement. + * The value you return will bypass merging logic and simply overwrite the property. + */ +export type FunctionPatch = (val: T, merge: Merge) => T; + +/** + * If you want to replace a array specify new array as the value. + * + * If you want edit array's item or insert new item specify object as the value. + * Keys of this object are array's indexes, values are patches of array's items. + */ +export type ArrayPatch = T extends Array ? ObjectPatch> : never; + +/** + * Mergerino merges immutably meaning that the target object will never be mutated (changed). + * Instead each object along the path your patch specifies will be shallow copied into a new object. + */ +export type NestedPatch = T extends object ? ObjectPatch : never; + +/** + * 1. Each object along the path your patch specifies will be shallow copied into a new object. + * 2. Specify undefined as the value fully remove a property from an object. + * 3. Use a function if you want to replace a property based on its current value. + */ +export type ObjectPatch = { [K in keyof S]?: S[K] | DeletePatch | FunctionPatch | NestedPatch | ArrayPatch }; + +/** + * Falsy patches are ignored + */ +export type Falsy = false | 0 | '' | null | undefined; + +/** + * Passing a function as a top level patch acts exactly the same as a function + * passed to a specific property. It receives the full state object as the first + * argument, the merge function as the second. + */ +export type TopLevelPatch = FunctionPatch | ObjectPatch | ArrayPatch | Falsy; + +/** + * You can pass multiple patches in a single merge call, array arguments will + * be flattened before processing. + */ +export type MultipleTopLevelPatch = TopLevelPatch | DeepArray>; + +/** + * Main Mergerino function. An immutable merge util for state management. + * + * You can pass multiple patches in a single merge call, array arguments will be flattened before processing. + * Since falsy patches are ignored. + */ +export type Merge = (source: S, ...patches: Array>) => S; + +/** + * Main Mergerino function. An immutable merge util for state management. + * + * You can pass multiple patches in a single merge call, array arguments will be flattened before processing. + * Since falsy patches are ignored. + */ +// tslint:disable-next-line:npm-naming +export default function merge(source: S, ...patches: Array>): S; +// Mergerino uses "default export", but no in minified version which is checked by dtslint. +// This line supress error: The types for mergerino specify 'export default' but the source does not mention 'default' anywhere. diff --git a/types/mergerino/mergerino-tests.ts b/types/mergerino/mergerino-tests.ts new file mode 100644 index 0000000000..43cc95a320 --- /dev/null +++ b/types/mergerino/mergerino-tests.ts @@ -0,0 +1,216 @@ +import merge from 'mergerino'; + +function deletingWorks() { + interface State { + deep: { + prop?: string; + }; + fake?: any; + other: boolean | null; + prop: boolean; + } + + const state: State = { + prop: true, + other: true, + deep: { prop: 'foo' }, + }; + + const newState = merge(state, { + deep: { prop: undefined }, + fake: undefined, // deleting non existent key + other: null, + prop: undefined, + }); +} + +function functionSubWorks() { + interface State { + age: number; + name: string; + obj: { + prop?: boolean; + replaced?: boolean; + }; + } + + const state: State = { + age: 10, + name: 'bob', + obj: { prop: true } + }; + + const newState = merge(state, { + age: x => x * 10, + name: (x, m) => { + return x; + }, + obj: () => ({ replaced: true }), + }); +} + +function deepFunctionSubToUncreatedObjectPath() { + interface State { + add?: { + stats: { + count: number; + }; + }; + orig: boolean; + } + + const state: State = { + orig: true + }; + + const newState = merge( + state, + { + add: { + stats: { + count: x => x + 1 + }, + } + } + ); +} + +function addNestedObject() { + interface State { + age: number; + add?: { + sub: boolean; + }; + } + const state: State = { age: 10 }; + const add = { sub: true }; + const newState = merge(state, { add }); +} + +function deepMergeObjects() { + interface State { + age: number; + sub: { + sub: { + prop: boolean; + newProp?: boolean; + }; + }; + } + + const state: State = { + age: 10, sub: { + sub: { prop: true }, + }, + }; + + const newState = merge( + state, + { + sub: { + sub: + { + newProp: true, + } + }, + }, + ); +} + +function functionPatch() { + interface State { + age: number; + foo: string; + prop?: boolean; + } + const state: State = { age: 10, foo: 'bar' }; + const newState = merge(state, (s, m) => { + return merge(s, { prop: true }); + }); +} + +function multiArrayFalsyPatches() { + interface State { + age?: number; + foo: string; + baz?: number; + hello?: boolean; + arr?: number[]; + prop?: boolean; + } + const state: State = { foo: 'bar' }; + const newState = merge( + state, + { baz: 5 }, + { hello: false }, + [{ arr: [1, 2, 3] }, [[{ prop: true }]], false, null], + undefined, + '', + 0, + null, + (s, m) => m(s, { age: 10 }), + [[[[[[[{ age: (x: number) => x * 3 }]]]]]]], + ); +} + +function arrayPatches() { + const arr = [1, 2, 3]; + const newArr = merge(arr, { 2: 100 }, { 0: undefined }, { 0: 200 }); +} + +function deepMergeWithArr() { + interface State { + foo: string; + deep: { + arr: number[]; + prop: boolean; + }; + } + const state: State = { foo: 'bar', deep: { arr: [1, 2, 3], prop: false } }; + const newState = merge(state, { deep: { arr: { 1: 20 } } }); +} + +// ToDo: It shoudn't be allowed, but it occurs when I use "infer" to get array type +function arrayObjectPatchNonExisitngProperty() { + interface State { + arr: Array<{ + prop: boolean; + }>; + } + const state: State = { arr: [{ prop: true }] }; + const newState = merge(state, { arr: { 0: { prop: false, nonExists: 42 } } }); +} + +function topLevelFunctionPatch() { + type State = + | { + age: number; + foo: string; + } + | { replaced: boolean }; + const state = { age: 20, foo: 'bar' }; + const replacement = { replaced: true }; + const newState = merge(state, () => replacement); +} + +function reuseObjectIfSameRefWhenPatching() { + const state = { deep: { prop: true } }; + const newState = merge(state, { deep: state.deep }); +} + +function replacePrimitiveWithObjectAndViceVersa() { + interface State { + count: + | number + | { + prop: boolean; + }; + foo: + | number + | { + prop: boolean; + }; + } + const state: State = { count: 10, foo: { prop: true } }; + const newState = merge(state, { count: { prop: true }, foo: 10 }); +} diff --git a/types/mergerino/tsconfig.json b/types/mergerino/tsconfig.json new file mode 100644 index 0000000000..43c81dcf24 --- /dev/null +++ b/types/mergerino/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "mergerino-tests.ts" + ] +} \ No newline at end of file diff --git a/types/mergerino/tslint.json b/types/mergerino/tslint.json new file mode 100644 index 0000000000..6746359dda --- /dev/null +++ b/types/mergerino/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "dtslint/dt.json" +}