From 0cb66d04e9897b5f3a79cb7cf9cd3347d837028e Mon Sep 17 00:00:00 2001 From: rawpixel-vincent Date: Mon, 4 Mar 2024 02:26:11 +0700 Subject: [PATCH] allow list or string in concat arguments --- .github/workflows/tests.yml | 2 +- README.md | 32 ++++++----- StringLiteralList.d.ts | 15 +++-- StringLiteralList.js | 43 ++++++++------- package.json | 4 +- stringList.d.ts | 2 +- stringList.js | 4 +- stringList.test.js | 106 ++++++++++++++++++++++++++++++------ 8 files changed, 144 insertions(+), 64 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a693566..dd0742b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: ['20', 'latest'] + node-version: ['16', '18', '20', 'latest'] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/README.md b/README.md index 08286d4..1eb28e4 100644 --- a/README.md +++ b/README.md @@ -73,15 +73,15 @@ it omits the methods that mutate the array in place like push, pop, shift, unshi ```js import { stringList } from 'string-literal-list'; -let v = stringList("foo", "bar", ...) => StringList<"foo" | "bar">; +let v = stringList("foo", "bar", ...) => SL<"foo" | "bar">; v.includes(anyValue) => boolean; -v.withPrefix('prefix.') => StringList<"prefix.foo" | "prefix.bar"> +v.withPrefix('prefix.') => SL<"prefix.foo" | "prefix.bar"> -v.withSuffix('.suffix') => StringList<"foo.suffix" | "bar.suffix"> +v.withSuffix('.suffix') => SL<"foo.suffix" | "bar.suffix"> -v.concat('zing', 'boom') => StringList<"foo" | "bar" | "zing" | "boom"> +v.concat('zing', 'boom') => SL<"foo" | "bar" | "zing" | "boom"> ``` @@ -125,16 +125,19 @@ list.includes(val); // No type error just boolean result. // list implements similar fix for indexOf, lastIndexOf, filter, some, every, findIndex and find methods. ``` -#### list.concat vs array.concat +#### list.concat(...(string|StringList)[]) -list.concat require string literals arguments to enable inference. +`list.concat()` accept only string and StringList as arguments to enable inference. +If a native array is passed the string literals won't be inferred. ```js // OK -list.concat('zing', 'foo'); +list.concat('zing', 'foo', stringList('gurgle', 'doink')); +=> SL<"foo" | "bar" | 'zing' | 'gurgle' | 'doink'> -// ERROR -list.concat(['zing', 'foo']); +// Not OK. +list.concat('zing', 'foo', ['boom', 'bar']); +// => Argument of type '"foo"' is not assignable to parameter of type '"zing" | ILiterals<"zing">'.ts(2345) ``` ### filter / map / reduce and other array methods @@ -161,8 +164,8 @@ namespace specs { /** * @description - * These methods are implemented in StringList class to change the returned type to IStringList. - * The execution is delegated to the Array instance and the result is used to construct the returned IStringList. + * The execution is delegated to the Array instance methods. + * The implementation uses the result to return a new readonly list instance. */ type ImplementedMethod = | 'concat' @@ -173,7 +176,7 @@ namespace specs { /** * @description - * These methods only get a type override to fix the comparison between `T` and `string`. + * The type of these methods are updated to fix the ts error comparison between `T` and `string`. */ type NativeMethodWithTypeOverride = | 'at' @@ -190,9 +193,8 @@ namespace specs { /** * @description - * These methods are the same are coming from the Array instance. - * No type overrides here. - * They will return the original type, (e.g. mutable array in case of map / reduce and other transforming methods.) + * No type or any overrides. + * filter, map, flatMap, flat will result in the original array type. */ type NativeMethod = | 'join' diff --git a/StringLiteralList.d.ts b/StringLiteralList.d.ts index ab483db..ce4bd35 100644 --- a/StringLiteralList.d.ts +++ b/StringLiteralList.d.ts @@ -1,12 +1,15 @@ import { sl } from './types.js'; +interface ILiterals { + literal: T; +} export interface IStringList extends Omit< Array, | sl.specs.ImplementedMethod | sl.specs.OmitedMutableMethod | sl.specs.NativeMethodWithTypeOverride - > { + >, ILiterals { // Custom Methods withPrefix

( prefix: P, @@ -19,9 +22,11 @@ export interface IStringList // Implemented methods to return the frozen array, typed as IStringList. toSorted(compareFn?: (a: T, b: T) => number): IStringList; toReversed(): IStringList; - concat( - ...arg: P[] - ): IStringList[keyof Record] | PP>; + // concat( + // ...arg: P[] + // ): IStringList[keyof Record] | PP>; + + concat(...arg: (ILiterals | S)[]): Readonly>; // Readonly overrides readonly length: number; @@ -79,7 +84,7 @@ export interface IStringList toSpliced(start: number, deleteCount: number, ...items: string[]): string[]; } -export class StringLiteralList { +export class SL { constructor(...list: T[]); } diff --git a/StringLiteralList.js b/StringLiteralList.js index 40086eb..d9a9947 100644 --- a/StringLiteralList.js +++ b/StringLiteralList.js @@ -1,32 +1,30 @@ -export class StringLiteralList extends Array { - concat() { - return Object.freeze( - new StringLiteralList(...super.concat.apply(this, arguments)), - ); +export class SL extends Array { + concat(...args) { + return Object.freeze(new SL(...super.concat.apply(this, args.flat()))); } toSorted() { - return Object.freeze( - new StringLiteralList(...super.toSorted.apply(this, arguments)), - ); + if (Array.prototype.toSorted) { + return Object.freeze(new SL(...super.toSorted.apply(this, arguments))); + } + const mut = this.mutable(); + return Object.freeze(new SL(...mut.sort.apply(mut, arguments))); } toReversed() { - return Object.freeze( - new StringLiteralList(...super.toReversed.apply(this, arguments)), - ); + if (Array.prototype.toReversed) { + return Object.freeze(new SL(...super.toReversed.apply(this, arguments))); + } + const mut = this.mutable(); + return Object.freeze(new SL(...mut.reverse.apply(mut, arguments))); } withPrefix(prefix) { - return Object.freeze( - new StringLiteralList(...super.map((e) => `${prefix}${e}`)), - ); + return Object.freeze(new SL(...super.map((e) => `${prefix}${e}`))); } withSuffix(suffix) { - return Object.freeze( - new StringLiteralList(...super.map((e) => `${e}${suffix}`)), - ); + return Object.freeze(new SL(...super.map((e) => `${e}${suffix}`))); } // Get the native array @@ -60,8 +58,13 @@ export class StringLiteralList extends Array { return mut.flatMap.apply(mut, arguments); } toSpliced() { + if (Array.prototype.toSpliced) { + const mut = this.mutable(); + return mut.toSpliced.apply(mut, arguments); + } const mut = this.mutable(); - return mut.toSpliced.apply(mut, arguments); + mut.splice.apply(mut, arguments); + return mut; } } @@ -78,7 +81,7 @@ export const ARRAY_IN_PLACE_MUTATION = Object.freeze({ reverse: 'reverse', }); Object.values(ARRAY_IN_PLACE_MUTATION).forEach((el) => { - StringLiteralList.prototype[el] = () => { - throw new Error(`Array method ${el} is not supported by StringLiteralList`); + SL.prototype[el] = () => { + throw new Error(`Array method ${el} is not supported by SL`); }; }); diff --git a/package.json b/package.json index 4b67177..5e1a7fb 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,13 @@ } }, "engines": { - "node": ">=20" + "node": ">=16" }, "packageManager": "npm@10.5.0+sha256.17ca6e08e7633b624e8f870db81a78f46afe119de62bcaf0a7407574139198fc", "license": "MIT", "scripts": { "test": "npm run lint:ci && npm run test:checkJs && npm run test:unit", - "test:unit": "tap run && tap report --show-full-coverage", + "test:unit": "tap run --allow-incomplete-coverage", "test:checkJs": "tsc --checkJs --project ./jsconfig.json", "prettier": "prettier --write \"**/*.{js,ts}\"", "lint": "eslint --fix \"./*.js\"", diff --git a/stringList.d.ts b/stringList.d.ts index 370e557..e655fbb 100644 --- a/stringList.d.ts +++ b/stringList.d.ts @@ -1,5 +1,5 @@ import { IStringList } from './StringLiteralList.js'; -export function stringList( +export function stringList( ...strings: T[] ): Readonly[T]>>; diff --git a/stringList.js b/stringList.js index e6f603c..d98af19 100644 --- a/stringList.js +++ b/stringList.js @@ -1,6 +1,6 @@ /// -import { StringLiteralList } from './StringLiteralList.js'; +import { SL } from './StringLiteralList.js'; /** @type {import('./stringList.js').stringList} */ export function stringList(...strings) { @@ -8,5 +8,5 @@ export function stringList(...strings) { throw new Error(`Not a string in stringList ${strings[0]}`); } // @ts-expect-error[2322] - return Object.freeze(new StringLiteralList(...strings)); + return Object.freeze(new SL(...strings)); } diff --git a/stringList.test.js b/stringList.test.js index dce8006..53bb8dd 100644 --- a/stringList.test.js +++ b/stringList.test.js @@ -1,27 +1,23 @@ import t from 'tap'; -import { - StringLiteralList, - ARRAY_IN_PLACE_MUTATION, -} from './StringLiteralList.js'; +import { SL, ARRAY_IN_PLACE_MUTATION } from './StringLiteralList.js'; import { stringList } from './stringList.js'; /** - * @template {string} TS - * @template {import('./StringLiteralList.js').IStringList} T * @param {import('tap').Test} st - * @param {T} list - * @param {...TS} values + * @param {any} list + * @param {...string} values */ const testExpectedArrayValues = (st, list, ...values) => { - st.ok(list.constructor.name === 'StringLiteralList'); + st.ok(list.constructor.name === 'SL'); st.notOk(list.constructor.name === 'Array'); - st.match(list, new StringLiteralList(...values)); + st.match(list, new SL(...values)); st.match(list, { length: values.length, }); st.match([...list], values); + st.match(JSON.stringify([...list]), JSON.stringify(values)); st.match([...list.keys()], [...values.keys()]); st.match([...list.values()], values); st.match([...list.entries()], [...values.entries()]); @@ -30,7 +26,9 @@ const testExpectedArrayValues = (st, list, ...values) => { st.ok(list.includes(value)); st.ok(list.indexOf(values[i]) === i); st.ok(list.at(i) === value); - st.ok(list.findLastIndex((v) => v === value) === i); + if (Array.prototype.findLastIndex) { + st.ok(list.findLastIndex((v) => v === value) === i); + } st.ok(list.findIndex((v) => v === value) === i); st.ok(list.some((v) => v === value) === true); st.ok(list.every((v) => v === value) === (list.length === 1)); @@ -41,22 +39,23 @@ const testExpectedArrayValues = (st, list, ...values) => { }; /** - * @template {string} TS - * @template {import('./StringLiteralList.js').IStringList} T * @param {import('tap').Test} st - * @param {T} list - * @param {...TS} values + * @param {any} list + * @param {...string} values */ const testEscapingFromStringList = (st, list, ...values) => { - st.ok(list.constructor.name === 'StringLiteralList'); + st.ok(list.constructor.name === 'SL'); // map() const fromMap = list.map((el) => el); st.ok(fromMap.constructor.name === 'Array'); st.ok(fromMap.length === list.length); st.match(fromMap, list); + st.match(JSON.stringify([...list]), JSON.stringify(fromMap)); if (values.length > 0) { - list.map((e) => e.toUpperCase()).includes(values[0].toUpperCase()); + list + .map((e) => typeof e === 'string' && e.toUpperCase()) + .includes(values[0].toUpperCase()); } // filter() @@ -64,36 +63,42 @@ const testEscapingFromStringList = (st, list, ...values) => { st.ok(fromFilter.constructor.name === 'Array'); st.ok(fromFilter.length === list.length); st.match(fromFilter, list); + st.match(JSON.stringify([...list]), JSON.stringify(fromFilter)); // reduce() const fromReduce = list.reduce((acc, el) => acc.concat(el), []); st.ok(fromReduce.constructor.name === 'Array'); st.ok(fromReduce.length === list.length); st.match(fromReduce, list); + st.match(JSON.stringify([...list]), JSON.stringify(fromReduce)); // reduceRight() const fromReduceRight = list.reduceRight((acc, el) => acc.concat(el), []); st.ok(fromReduceRight.constructor.name === 'Array'); st.ok(fromReduceRight.length === list.length); st.match(fromReduceRight.reverse(), list); + st.match(JSON.stringify([...list]), JSON.stringify(fromReduceRight)); // flat() const fromFlat = list.flat(); st.ok(fromFlat.constructor.name === 'Array'); st.ok(fromFlat.length === list.length); st.match(fromFlat, list); + st.match(JSON.stringify([...list]), JSON.stringify(fromFlat)); // flatMap() const fromFlatMap = list.flatMap((el) => [el]); st.ok(fromFlatMap.constructor.name === 'Array'); st.ok(fromFlatMap.length === list.length); st.match(fromFlatMap, list); + st.match(JSON.stringify([...list]), JSON.stringify(fromFlatMap)); // toSpliced() const fromSpliced = list.toSpliced(0, 0); st.ok(fromSpliced.constructor.name === 'Array'); st.ok(fromSpliced.length === list.length); st.match(fromSpliced, list); + st.match(JSON.stringify([...list]), JSON.stringify(fromSpliced)); }; t.test('empty stringList', (t) => { @@ -101,6 +106,19 @@ t.test('empty stringList', (t) => { t.match([...list], []); testExpectedArrayValues(t, list); testEscapingFromStringList(t, list); + t.doesNotThrow(() => { + // t.ok(list.concat().length === 0); + // t.ok(list.withPrefix('.').length === 0); + // t.ok(list.withSuffix('.').length === 0); + // t.ok(list.toSorted().length === 0); + // t.ok(list.toReversed().length === 0); + }); + const notEmpty = list.concat('a', 'b', 'c'); + t.ok(notEmpty.length === 3); + t.ok(list.length === 0); + t.notMatch(list, notEmpty); + t.match(notEmpty, stringList('a', 'b', 'c')); + t.end(); }); @@ -139,6 +157,48 @@ t.test("concat('zing', 'boom')", (t) => { t.end(); }); +t.test("concat(stringList, stringList, 'a', 'b', 'c', 'd')", (t) => { + const a = stringList('abc', 'def', 'ghi'); + const b = stringList('jkl', 'mno', 'pqr'); + const c = stringList('stu', 'vwx', 'yz'); + const list = a.concat(b, c, 'a', 'b', 'c', 'd'); + testExpectedArrayValues( + t, + list, + 'abc', + 'def', + 'ghi', + 'jkl', + 'mno', + 'pqr', + 'stu', + 'vwx', + 'yz', + 'a', + 'b', + 'c', + 'd', + ); + testEscapingFromStringList( + t, + list, + 'abc', + 'def', + 'ghi', + 'jkl', + 'mno', + 'pqr', + 'stu', + 'vwx', + 'yz', + 'a', + 'b', + 'c', + 'd', + ); + t.end(); +}); + t.test('toSorted()', (t) => { const list = stringList('foo', 'bar').toSorted(); testExpectedArrayValues(t, list, 'bar', 'foo'); @@ -196,10 +256,20 @@ t.test('stringList mutable', (t) => { () => // @ts-expect-error list[el](), - new Error(`Array method ${el} is not supported by StringLiteralList`), + new Error(`Array method ${el} is not supported by SL`), ); // @ts-expect-error t.ok(list.mutable()[el]()); }); t.end(); }); + +t.test('fallback for modern array methods', (t) => { + Array.prototype.toReversed = null; + Array.prototype.toSorted = null; + Array.prototype.toSpliced = null; + + stringList('foo', 'bar').toSorted().toReversed().toSpliced(1, 1); + + t.end(); +});