Skip to content

Commit

Permalink
allow list or string in concat arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
rawpixel-vincent committed Mar 3, 2024
1 parent dfd40fa commit 0cb66d0
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 64 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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">

```

Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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'
Expand All @@ -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'
Expand Down
15 changes: 10 additions & 5 deletions StringLiteralList.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { sl } from './types.js';

interface ILiterals<T extends unknown = null> {
literal: T;
}
export interface IStringList<T extends unknown>
extends Omit<
Array<T>,
| sl.specs.ImplementedMethod
| sl.specs.OmitedMutableMethod
| sl.specs.NativeMethodWithTypeOverride
> {
>, ILiterals<T> {
// Custom Methods
withPrefix<P extends string>(
prefix: P,
Expand All @@ -19,9 +22,11 @@ export interface IStringList<T extends unknown>
// Implemented methods to return the frozen array, typed as IStringList.
toSorted(compareFn?: (a: T, b: T) => number): IStringList<T>;
toReversed(): IStringList<T>;
concat<PP extends T, P extends string = string>(
...arg: P[]
): IStringList<Record<P, P>[keyof Record<P, P>] | PP>;
// concat<PP extends T, P extends string = string>(
// ...arg: P[]
// ): IStringList<Record<P, P>[keyof Record<P, P>] | PP>;

concat<PP extends T, S extends string>(...arg: (ILiterals<S> | S)[]): Readonly<IStringList<PP | S>>;

// Readonly overrides
readonly length: number;
Expand Down Expand Up @@ -79,7 +84,7 @@ export interface IStringList<T extends unknown>
toSpliced(start: number, deleteCount: number, ...items: string[]): string[];
}

export class StringLiteralList<T extends string> {
export class SL<T extends string> {
constructor(...list: T[]);
}

Expand Down
43 changes: 23 additions & 20 deletions StringLiteralList.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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`);
};
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
}
},
"engines": {
"node": ">=20"
"node": ">=16"
},
"packageManager": "[email protected]+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\"",
Expand Down
2 changes: 1 addition & 1 deletion stringList.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IStringList } from './StringLiteralList.js';

export function stringList<T extends string>(
export function stringList<T extends string = never>(
...strings: T[]
): Readonly<IStringList<Record<T, T>[T]>>;
4 changes: 2 additions & 2 deletions stringList.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/// <reference path="types.d.ts" />

import { StringLiteralList } from './StringLiteralList.js';
import { SL } from './StringLiteralList.js';

/** @type {import('./stringList.js').stringList} */
export function stringList(...strings) {
if (strings.some((el) => typeof el !== 'string')) {
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));
}
Loading

0 comments on commit 0cb66d0

Please sign in to comment.