From 77eebb06a59b3da45f552653830dafc82c0790ff Mon Sep 17 00:00:00 2001 From: Guillermo Grau Date: Wed, 22 Jul 2020 08:39:07 +0200 Subject: [PATCH] Types: update input data types to avoid problems with user-defined interfaces --- src/timm.ts | 195 ++++++++++++++++++++++----------------------- tools/typeTests.ts | 37 +++++++++ 2 files changed, 134 insertions(+), 98 deletions(-) diff --git a/src/timm.ts b/src/timm.ts index 676a207..0eefdd2 100755 --- a/src/timm.ts +++ b/src/timm.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/ban-types */ + /*! * Timm * @@ -9,7 +11,6 @@ const INVALID_ARGS = 'INVALID_ARGS'; -type TimmObject = Record; type Key = string | number; // =============================================== @@ -19,7 +20,7 @@ function throwStr(msg: string): never { throw new Error(msg); } -function getKeysAndSymbols(obj: any): string[] { +function getKeysAndSymbols(obj: object): string[] { const keys = Object.keys(obj); if (Object.getOwnPropertySymbols) { // @ts-ignore @@ -30,16 +31,14 @@ function getKeysAndSymbols(obj: any): string[] { const hasOwnProperty = {}.hasOwnProperty; -export function clone(obj: T[]): T[]; -export function clone(obj: T): T; -export function clone(obj0: T | T[]): T | T[] { +export function clone(obj0: T): T { // As array - if (Array.isArray(obj0)) return obj0.slice(); + if (Array.isArray(obj0)) return obj0.slice() as T; // As object - const obj = obj0 as TimmObject; + const obj: any = obj0; const keys = getKeysAndSymbols(obj); - const out = {} as TimmObject; + const out: any = {}; for (let i = 0; i < keys.length; i++) { const key = keys[i]; out[key] = obj[key]; @@ -49,7 +48,7 @@ export function clone(obj0: T | T[]): T | T[] { } // Custom guard -function isObject(o: unknown): o is TimmObject { +function isObject(o: unknown): any { return o != null && typeof o === 'object'; } @@ -68,7 +67,7 @@ function isObject(o: unknown): o is TimmObject { // -- #### addLast() // -- Returns a new array with an appended item or items. // -- -// -- Usage: `addLast(array: T[], val: T[] | T): T[]` +// -- Usage: `addLast(array, val)` // -- // -- ```js // -- arr = ['a', 'b'] @@ -89,7 +88,7 @@ export function addLast(array: T[], val: T[] | T): T[] { // -- #### addFirst() // -- Returns a new array with a prepended item or items. // -- -// -- Usage: `addFirst(array: T[], val: T[]|T): T[]` +// -- Usage: `addFirst(array, val)` // -- // -- ```js // -- arr = ['a', 'b'] @@ -108,7 +107,7 @@ export function addFirst(array: T[], val: T[] | T): T[] { // -- #### removeLast() // -- Returns a new array removing the last item. // -- -// -- Usage: `removeLast(array: T[]): T[]` +// -- Usage: `removeLast(array)` // -- // -- ```js // -- arr = ['a', 'b'] @@ -130,7 +129,7 @@ export function removeLast(array: T[]): T[] { // -- #### removeFirst() // -- Returns a new array removing the first item. // -- -// -- Usage: `removeFirst(array: T[]): T[]` +// -- Usage: `removeFirst(array)` // -- // -- ```js // -- arr = ['a', 'b'] @@ -153,7 +152,7 @@ export function removeFirst(array: T[]): T[] { // -- Returns a new array obtained by inserting an item or items // -- at a specified index. // -- -// -- Usage: `insert(array: T[], idx: number, val: T[] | T): T[]` +// -- Usage: `insert(array, idx, val)` // -- // -- ```js // -- arr = ['a', 'b', 'c'] @@ -175,7 +174,7 @@ export function insert(array: T[], idx: number, val: T[] | T): T[] { // -- Returns a new array obtained by removing an item at // -- a specified index. // -- -// -- Usage: `removeAt(array: T[], idx: number): T[]` +// -- Usage: `removeAt(array, idx)` // -- // -- ```js // -- arr = ['a', 'b', 'c'] @@ -199,7 +198,7 @@ export function removeAt(array: T[], idx: number): T[] { // -- (*referentially equal to*) the previous item at that position, // -- the original array is returned. // -- -// -- Usage: `replaceAt(array: T[], idx: number, newItem: T): T[]` +// -- Usage: `replaceAt(array, idx, newItem)` // -- // -- ```js // -- arr = ['a', 'b', 'c'] @@ -226,18 +225,12 @@ export function replaceAt(array: T[], idx: number, newItem: T): T[] { // =============================================== // -- ### Collections (objects and arrays) // =============================================== -// -- The following types are used throughout this section -// -- ```js -// -- type TimmObject = Record; -// -- type Key = number | string; -// -- ``` - // -- #### getIn() // -- Returns a value from an object at a given path. Works with // -- nested arrays and objects. If the path does not exist, it returns // -- `undefined`. // -- -// -- Usage: `getIn(obj: TimmObject, path: Key[]): unknown` +// -- Usage: `getIn(obj, path)` // -- // -- ```js // -- obj = { a: 1, b: 2, d: { d1: 3, d2: 4 }, e: ['a', 'b', 'c'] } @@ -248,7 +241,7 @@ export function replaceAt(array: T[], idx: number, newItem: T): T[] { // -- ``` export function getIn(obj: undefined, path: Key[]): undefined; export function getIn(obj: null, path: Key[]): null; -export function getIn(obj: any[] | TimmObject, path: Key[]): unknown; +export function getIn(obj: object, path: Key[]): unknown; export function getIn(obj: any, path: Key[]): unknown { if (!Array.isArray(path)) { throwStr( @@ -272,8 +265,7 @@ export function getIn(obj: any, path: Key[]): unknown { // -- If the provided value is the same as (*referentially equal to*) // -- the previous value, the original object is returned. // -- -// -- Usage: `set(obj: T, key: string, val: T): T` -// -- Or with an array: `set(obj: T[], key: number, val: T): T[]` +// -- Usage: `set(obj, key, val)` // -- // -- ```js // -- obj = { a: 1, b: 2, c: 3 } @@ -295,7 +287,7 @@ export function set( ): { [P in K]: V }; export function set(obj: undefined | null, key: number, val: V): [V]; // Normal operation with an object/array -export function set( +export function set( obj: T, key: K, val: V @@ -321,7 +313,7 @@ export function set(obj0: any, key: Key, val: any): any { // -- * If the path does not exist, it will be created before setting // -- the new value. // -- -// -- Usage: `setIn(obj: any, path: Key[], val: any): unknown` +// -- Usage: `setIn(obj, path, val)` // -- // -- ```js // -- obj = { a: 1, b: 2, d: { d1: 3, d2: 4 }, e: { e1: 'foo', e2: 'bar' } } @@ -348,26 +340,35 @@ export function set(obj0: any, key: Key, val: any): any { // -- setIn({ a: 3 }, ['unknown', 0, 'path'], 4) // -- // { a: 3, unknown: [{ path: 4 }] } // -- ``` -export function setIn(obj: any, path: Key[], val: any): unknown { +export function setIn( + obj: object | null | undefined, + path: Key[], + val: any +): unknown { if (!path.length) return val; return doSetIn(obj, path, val, 0); } -function doSetIn(obj: any, path: Key[], val: any, idx: number): unknown { +function doSetIn( + obj: object | null | undefined, + path: Key[], + val: any, + idx: number +): unknown { let newValue; const key: any = path[idx]; if (idx === path.length - 1) { newValue = val; } else { const nestedObj = - isObject(obj) && isObject(obj[key]) - ? obj[key] + isObject(obj) && isObject((obj as any)[key]) + ? (obj as any)[key] : typeof path[idx + 1] === 'number' ? [] : {}; newValue = doSetIn(nestedObj, path, val, idx + 1); } - return set(obj, key, newValue); + return set(obj as any, key, newValue); } // -- #### update() @@ -376,8 +377,7 @@ function doSetIn(obj: any, path: Key[], val: any, idx: number): unknown { // -- If the calculated value is the same as (*referentially equal to*) // -- the previous value, the original object is returned. // -- -// -- Usage: `update(obj: any, key: Key, -// -- fnUpdate: (prevValue: any) => any): unknown` +// -- Usage: `update(obj, key, fnUpdate)` // -- // -- ```js // -- obj = { a: 1, b: 2, c: 3 } @@ -391,13 +391,13 @@ function doSetIn(obj: any, path: Key[], val: any, idx: number): unknown { // -- // true // -- ``` export function update( - obj: any, + obj: object | null | undefined, key: Key, fnUpdate: (prevValue: any) => any ): unknown { - const prevVal = obj == null ? undefined : obj[key]; + const prevVal = obj == null ? undefined : (obj as any)[key]; const nextVal = fnUpdate(prevVal); - return set(obj, key as any, nextVal); + return set(obj as any, key as any, nextVal); } // -- #### updateIn() @@ -423,11 +423,11 @@ export function update( // -- // true // -- ``` export function updateIn( - obj: any, + obj: object | null | undefined, path: Key[], fnUpdate: (prevValue: any) => any ): unknown { - const prevVal = getIn(obj, path); + const prevVal = getIn(obj as any, path); const nextVal = fnUpdate(prevVal); return setIn(obj, path, nextVal); } @@ -439,8 +439,8 @@ export function updateIn( // -- // -- Usage: // -- -// -- * `merge(obj1: TimmObject, obj2: TimmObject): TimmObject` -// -- * `merge(obj1: TimmObject, ...objects: TimmObject[]): TimmObject` +// -- * `merge(obj1, obj2)` +// -- * `merge(obj1, ...objects)` // -- // -- The unmodified `obj1` is returned if `obj2` does not *provide something // -- new to* `obj1`, i.e. if either of the following @@ -470,43 +470,43 @@ export function updateIn( // Signatures: // - 1 arg -export function merge(a: T): T; +export function merge(a: T): T; // - 2 args -export function merge( +export function merge( a: T, b: U ): Omit & U; -export function merge(a: T, b: undefined | null): T; +export function merge(a: T, b: undefined | null): T; // - 3 args -export function merge< - T extends TimmObject, - U extends TimmObject, - V extends TimmObject ->(a: T, b: U, c: V): Omit & U, keyof V> & V; -export function merge( +export function merge( + a: T, + b: U, + c: V +): Omit & U, keyof V> & V; +export function merge( a: T, b: undefined | null, c: V ): Omit & V; -export function merge( +export function merge( a: T, b: U, c: undefined | null ): Omit & U; -export function merge( +export function merge( a: T, b: undefined | null, c: undefined | null ): T; // Implementation and catch-all export function merge( - a: TimmObject, - b?: TimmObject | null, - c?: TimmObject | null, - d?: TimmObject | null, - e?: TimmObject | null, - f?: TimmObject | null, - ...rest: Array + a: object, + b?: object | null, + c?: object | null, + d?: object | null, + e?: object | null, + f?: object | null, + ...rest: Array ): unknown { return rest.length ? doMerge.call(null, false, false, a, b, c, d, e, f, ...rest) @@ -521,8 +521,8 @@ export function merge( // -- // -- Usage: // -- -// -- * `mergeDeep(obj1: TimmObject, obj2: TimmObject): TimmObject` -// -- * `mergeDeep(obj1: TimmObject, ...objects: TimmObject[]): TimmObject` +// -- * `mergeDeep(obj1, obj2)` +// -- * `mergeDeep(obj1, ...objects)` // -- // -- The unmodified `obj1` is returned if `obj2` does not *provide something // -- new to* `obj1`, i.e. if either of the following @@ -550,14 +550,14 @@ export function merge( // -- // true // -- ``` export function mergeDeep( - a: TimmObject, - b?: TimmObject | null, - c?: TimmObject | null, - d?: TimmObject | null, - e?: TimmObject | null, - f?: TimmObject | null, - ...rest: Array -): TimmObject { + a: object, + b?: object | null, + c?: object | null, + d?: object | null, + e?: object | null, + f?: object | null, + ...rest: Array +): object { return rest.length ? doMerge.call(null, false, true, a, b, c, d, e, f, ...rest) : doMerge(false, true, a, b, c, d, e, f); @@ -568,8 +568,8 @@ export function mergeDeep( // -- // -- Usage examples: // -- -// -- * `mergeIn(obj1: TimmObject, path: Key[], obj2: TimmObject): TimmObject` -// -- * `mergeIn(obj1: TimmObject, path: Key[], ...objects: TimmObject[]): TimmObject` +// -- * `mergeIn(obj1, path, obj2)` +// -- * `mergeIn(obj1, path, ...objects)` // -- // -- ```js // -- obj1 = { a: 1, d: { b: { d1: 3, d2: 4 } } } @@ -586,12 +586,12 @@ export function mergeDeep( export function mergeIn( a: any, path: Key[], - b?: TimmObject, - c?: TimmObject, - d?: TimmObject, - e?: TimmObject, - f?: TimmObject, - ...rest: TimmObject[] + b?: object | null, + c?: object | null, + d?: object | null, + e?: object | null, + f?: object | null, + ...rest: Array ): unknown { let prevVal: any = getIn(a, path); if (prevVal == null) prevVal = {}; @@ -607,7 +607,7 @@ export function mergeIn( // -- #### omit() // -- Returns an object excluding one or several attributes. // -- -// -- Usage: `omit(obj: TimmObject, attrs: string | string[]): TimmObject` +// -- Usage: `omit(obj, attrs)` // // -- ```js // -- obj = { a: 1, b: 2, c: 3, d: 4 } @@ -620,11 +620,10 @@ export function mergeIn( // -- omit(obj, 'z') === obj1 // -- // true // -- ``` -export function omit( +export function omit( obj: T, - attr: K | K[] -): Omit; -export function omit(obj: TimmObject, attrs: string | string[]): unknown { + attrs: K | K[] +): Omit { const omitList = Array.isArray(attrs) ? attrs : [attrs]; let fDoSomething = false; for (let i = 0; i < omitList.length; i++) { @@ -637,9 +636,9 @@ export function omit(obj: TimmObject, attrs: string | string[]): unknown { const out: any = {}; const keys = getKeysAndSymbols(obj); for (let i = 0; i < keys.length; i++) { - const key = keys[i]; + const key: any = keys[i]; if (omitList.indexOf(key) >= 0) continue; - out[key] = obj[key]; + out[key] = (obj as any)[key]; } return out; } @@ -651,8 +650,8 @@ export function omit(obj: TimmObject, attrs: string | string[]): unknown { // -- // -- Usage: // -- -// -- * `addDefaults(obj: TimmObject, defaults: TimmObject): TimmObject` -// -- * `addDefaults(obj: TimmObject, ...defaultObjects: TimmObject[]): TimmObject` +// -- * `addDefaults(obj, defaults)` +// -- * `addDefaults(obj, ...defaultObjects)` // -- // -- ```js // -- obj1 = { a: 1, b: 2, c: 3 } @@ -668,19 +667,19 @@ export function omit(obj: TimmObject, attrs: string | string[]): unknown { // -- ``` // Signatures: // - 2 args -export function addDefaults( +export function addDefaults( a: T, b: U ): Omit & T; // Implementation and catch-all export function addDefaults( - a: TimmObject, - b: TimmObject, - c?: TimmObject | null, - d?: TimmObject | null, - e?: TimmObject | null, - f?: TimmObject | null, - ...rest: Array + a: object, + b: object, + c?: object | null, + d?: object | null, + e?: object | null, + f?: object | null, + ...rest: Array ): unknown { return rest.length ? doMerge.call(null, true, false, a, b, c, d, e, f, ...rest) @@ -690,8 +689,8 @@ export function addDefaults( function doMerge( fAddDefaults: boolean, fDeep: boolean, - first: TimmObject, - ...rest: Array + first: object, + ...rest: Array ) { let out: any = first; if (!(out != null)) { @@ -710,7 +709,7 @@ function doMerge( for (let j = 0; j <= keys.length; j++) { const key = keys[j]; if (fAddDefaults && out[key] !== undefined) continue; - let nextVal = obj[key]; + let nextVal = (obj as any)[key]; if (fDeep && isObject(out[key]) && isObject(nextVal)) { nextVal = doMerge(fAddDefaults, fDeep, out[key], nextVal); } diff --git a/tools/typeTests.ts b/tools/typeTests.ts index c31cccb..8a08433 100644 --- a/tools/typeTests.ts +++ b/tools/typeTests.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + import timm from '../src/timm'; // https://fettblog.eu/typescript-match-the-exact-object-shape/ @@ -10,10 +12,15 @@ type ValidateShape = T extends Shape const testArrays = () => { const arr = ['a', 3]; const obj = { foo: 'a', bar: { bar1: 4 } }; + interface Obj2 { + foo: string; + } + const obj2: Obj2 = { foo: 'b' }; // Clone const resA: Array = timm.clone(arr); const resB: { foo: string; bar: { bar1: number } } = timm.clone(obj); + const obj2clone: Obj2 = timm.clone(obj2); // Arrays const arr1a: Array = timm.addLast(arr, 'a'); @@ -33,6 +40,7 @@ const testArrays = () => { const res1a = timm.getIn(undefined, ['x']); const res1b = timm.getIn(null, ['x']); const res1c: string = timm.getIn(obj, ['foo']) as string; + const res1d: string = timm.getIn(obj2, ['foo']) as string; // set with undefined/null const res2a: [string] = timm.set(undefined, 0, 'a'); const res2b: [string] = timm.set(null, 0, 'a'); @@ -40,6 +48,8 @@ const testArrays = () => { const res2d: [null] = timm.set(null, 0, null); const res2e: { foo: number } = timm.set(undefined, 'foo', 3); const res2f: { foo: string } = timm.set(null, 'foo', 'x'); + // @ts-expect-error + const res2aInvalid = timm.set(true, 'foo', 'x'); // set with array const res2g: string[] = timm.set(['a', 'b'], 1, 'c'); const res2h: Array = timm.set(['a', 'b'], 1, null); @@ -69,3 +79,30 @@ const testArrays = () => { const res5b = timm.omit({ a: 3, b: 'foo', c: 5 }, ['b', 'c']); (function (_val: ValidateShape) {})(res5b); }; + +// type Dict = Record; +// type HelloType = { +// hello: string; +// }; +// interface HelloInterface { +// hello: string; +// } + +// let y: Dict; +// const hello1: HelloType = { hello: 'foo' }; +// const hello2: HelloInterface = { hello: 'foo' }; +// y = hello1; +// y = hello2; + +// let z: HelloInterface; +// z = hello1; +// z = hello2; + +// interface AA { +// foo: string; +// } +// interface BB extends AA { +// bar: number; +// } +// let b: BB; +// const a: AA = b;