Skip to content

Commit

Permalink
add strict/mutable function in two different export and remove protot…
Browse files Browse the repository at this point in the history
…ype overrides to prevent errors when frozen
  • Loading branch information
rawpixel-vincent committed Mar 6, 2024
1 parent 18099ed commit 7e2c31f
Show file tree
Hide file tree
Showing 13 changed files with 758 additions and 561 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ Useful for types constructs that can be used at runtime.

*If you code in typescript, you probably don't need any of this.*

Thanks to @gustavoguichard and his work on https://github.com/gustavoguichard/string-ts that taught me how to work with string literals.

## Overview

The StringList class extends the Array interface types to work with string literals.

- immutable: methods that mutate the array in place like push, pop, shift, unshift, splice are not working, it's possible to escape the list to a mutable array with the method `mutable()`
- immutable: methods that mutate the array in place like push, pop, shift, unshift, splice should not be used, there is a strict export that enforce this by freezing the instance when constructed.

- inference: concat, toReversed, toSorted and slice methods are implemented to return a new frozen instance and will infer the new values.
- the concat method parameters types has been updated to accept strings literals and/or instances of StringList.
Expand Down Expand Up @@ -56,6 +54,7 @@ yarn add string-literal-list

```js
import { stringList } from 'string-literal-list';
// Or import { stringList } from 'string-literal-list/strict.js'; with frozen array.

let v = stringList("foo", "bar", ...) => SL<"foo" | "bar">;

Expand Down Expand Up @@ -214,3 +213,7 @@ lit.concat(val);
// Overload 2 of 2, '(...items: ("foo" | "bar" | ConcatArray<"foo" | "bar">)[]): ("foo" | "bar")[]', gave the following error.
// Argument of type 'string' is not assignable to parameter of type '"foo" | "bar" | ConcatArray<"foo" | "bar">'.ts(2769)
```

## Credits

Thanks to @gustavoguichard and his work on https://github.com/gustavoguichard/string-ts that taught me how to work with string literals.
114 changes: 81 additions & 33 deletions StringLiteralList.d.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,86 @@
import { sl } from './types.js';

export type ArrayInPlaceMutation = {
push: 'push';
unshift: 'unshift';
shift: 'shift';
copyWithin: 'copyWithin';
pop: 'pop';
fill: 'fill';
splice: 'splice';
};

export const ARRAY_IN_PLACE_MUTATION: ArrayInPlaceMutation;


interface ILiterals<T extends string> {
literal: T;
}

export interface IStringList<T extends string>
export type MaybeReadonly<T extends boolean, A> = [T] extends [true] ? Readonly<A> : A;

export interface IStringList<T extends string, Mut extends boolean = false>
extends Omit<
Array<T>,
sl.specs.ImplementedMethod
>, ILiterals<T> {

// Custom Methods
withPrefix<P extends string>(
prefix: P,
): IStringList<sl.utils.StringConcat<P, T extends string ? T : string>>;
): MaybeReadonly<Mut, IStringList<sl.utils.StringConcat<P, T extends string ? T : string>, Mut>>;
withSuffix<P extends string>(
suffix: P,
): IStringList<sl.utils.StringConcat<T extends string ? T : string, P>>;
): MaybeReadonly<Mut, IStringList<sl.utils.StringConcat<T extends string ? T : string, P>, Mut>>;

withDerivatedSuffix<S extends string>(
chars: S
): IStringList<T | sl.utils.DropSuffix<sl.utils.DropSuffix<sl.utils.StringConcat<T, sl.utils.StringConcat<S, S>>, S>, sl.utils.StringConcat<S, S>>>;
): MaybeReadonly<Mut, IStringList<
| T
| sl.utils.DropSuffix<
sl.utils.DropSuffix<
`${T}${S}${S}`,
S
>,
`${S}${S}`
>,
Mut>>;

withDerivatedPrefix<S extends string>(
chars: S
): IStringList<T | sl.utils.DropPrefix<sl.utils.DropPrefix<sl.utils.StringConcat<sl.utils.StringConcat<S, S>, T>, S>, sl.utils.StringConcat<S, S>>>;
withReplace<
S extends string | RegExp,
D extends string
>(searchValue: S, replaceValue: D): IStringList<sl.utils.Replace<T, S, D>>;
withReplaceAll<
S extends string | RegExp,
D extends string
>(searchValue: S, replaceValue: D): IStringList<sl.utils.ReplaceAll<T, S, D>>;
withTrim(): IStringList<sl.utils.Trim<T>>;
): MaybeReadonly<Mut, IStringList<
T |
sl.utils.DropPrefix<
sl.utils.DropPrefix<
`${S}${S}${T}`,
S
>,
`${S}${S}`
>,
Mut>>;

withReplace<S extends string | RegExp, D extends string>(
searchValue: S,
replaceValue: D
): MaybeReadonly<Mut, IStringList<sl.utils.Replace<T, S, D>, Mut>>;

withReplaceAll<S extends string | RegExp, D extends string>(
searchValue: S,
replaceValue: D
): MaybeReadonly<Mut, IStringList<sl.utils.ReplaceAll<T, S, D>, Mut>>;

withTrim(): MaybeReadonly<Mut, IStringList<sl.utils.Trim<T>, Mut>>;
value(val): T;
mutable(): T & string[];
sort<P1 = T, P2 = T>(compareFn?: (a: P1, b: P2) => number): this;
reverse(): this;
without<PP extends T & string, S extends string>(...arg: (ILiterals<S> | S)[]): IStringList<Exclude<PP | S, S>>;
without<PP extends T & string, S extends string>(...arg: (ILiterals<S> | S)[]): MaybeReadonly<Mut, IStringList<Exclude<PP | S, S>, Mut>>;

// Implemented methods to return the frozen array, typed as IStringList.
toSorted(compareFn?: (a: T, b: T) => number): IStringList<T>;
toReversed(): IStringList<T>;
toSorted(compareFn?: (a: T, b: T) => number): MaybeReadonly<Mut, IStringList<T, Mut>>;
toReversed(): IStringList<T, Mut>;

concat<PP extends T, S extends string>(...arg: (ILiterals<S> | S)[]): IStringList<PP | S>;
concat<PP extends T, S extends string>(...arg: (ILiterals<S> | S)[]): MaybeReadonly<Mut, IStringList<PP | S, Mut>>;
// Readonly overrides
readonly length: number;
readonly [n: number]: T | undefined;
Expand Down Expand Up @@ -71,7 +110,7 @@ export interface IStringList<T extends string>
): T;
findIndex<S = T & string>(predicate: (value: S & string, index: number, obj: T[]) => unknown, thisArg?: any): number;

slice(start?: number, end?: number): IStringList<T>;
slice(start?: number, end?: number): MaybeReadonly<Mut, IStringList<T, Mut>>;

some<S = T & string>(predicate: (value: S & string, index: number, array: T[]) => unknown, thisArg?: any): boolean;
every<S = T & string>(predicate: (value: S & string, index: number, array: T[]) => value is S & string, thisArg?: any): this is S[];
Expand All @@ -89,21 +128,30 @@ export interface IStringList<T extends string>
thisArg?: any,
): T & string[];
toSpliced<S = T & string>(start: number, deleteCount: number, ...items: string[]): (S & string)[];
pop(): T;
pop(): [Mut] extends [true] ? T : never;
shift(): [Mut] extends [true] ? T : never;
unshift<S = T & string>(...newElement: S[]): [Mut] extends [true] ? number : never;
push<S = T & string>(...items: S[]): [Mut] extends [true] ? number : never;
splice(start: number, deleteCount?: number): [Mut] extends [true]
? T[]
: never;
copyWithin<S = T & string>(target: number, start: number, end?: number): [Mut] extends [true] ? S[] : never;
fill<S = T, U = T & string>(
value: (U | undefined)[],
start?: number,
end?: number,
): [Mut] extends [true] ? U[] : never;
fill<U = T & string>(
value: U,
start?: number,
end?: number,
): [Mut] extends [true] ? U[] : never;
// join<D extends string = ''>(delimiter?: D): sl.utils.Join<SS, D>;
}

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

export interface ArrayInPlaceMutation {
push: 'push';
unshift: 'unshift';
shift: 'shift';
copyWithin: 'copyWithin';
pop: 'pop';
fill: 'fill';
splice: 'splice';
export class SL {
literal: undefined;
constructor(...list: string[]);
mutable(): string[];
}

export const ARRAY_IN_PLACE_MUTATION: ArrayInPlaceMutation;
95 changes: 60 additions & 35 deletions StringLiteralList.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ import 'core-js/actual/array/to-sorted.js';
import 'core-js/actual/array/to-spliced.js';
import 'core-js/actual/array/with.js';

const freezeIfImmutable = (source, target) => {
if (Object.isFrozen(source)) {
return Object.freeze(target);
}
return target;
};

export class SL extends Array {
literal = undefined;
enum;
hasEmpty = false;
constructor(...args) {
super(...args);
this.enum = Object.fromEntries(
this.map((v) => {
if (v === '') {
this.hasEmpty = true;
const entries = [];
const arr = [];
let emptyFound = false;
for (const str of args.flat()) {
if (typeof str === 'string') {
if (str === '') {
emptyFound = true;
}
return [v, v];
}),
);
entries.push([str, str]);
arr.push(str);
}
}
super(...arr);
this.hasEmpty = emptyFound;
this.enum = Object.fromEntries(entries);

if (this.hasEmpty) {
this.enum[''] = '';
Expand All @@ -25,19 +38,31 @@ export class SL extends Array {
}

concat(...args) {
return Object.freeze(new SL(...super.concat.apply(this, args.flat())));
return freezeIfImmutable(
this,
new SL(...super.concat.apply(this, args.flat())),
);
}

toSorted() {
return Object.freeze(new SL(...super.toSorted.apply(this, arguments)));
return freezeIfImmutable(
this,
new SL(...super.toSorted.apply(this, arguments)),
);
}

toReversed() {
return Object.freeze(new SL(...super.toReversed.apply(this, arguments)));
return freezeIfImmutable(
this,
new SL(...super.toReversed.apply(this, arguments)),
);
}

slice() {
return Object.freeze(new SL(...super.slice.apply(this, arguments)));
return freezeIfImmutable(
this,
new SL(...super.slice.apply(this, arguments)),
);
}

without() {
Expand All @@ -48,23 +73,33 @@ export class SL extends Array {
? [el]
: [],
);
return Object.freeze(new SL(...this.filter((e) => !values.includes(e))));
return freezeIfImmutable(
this,
new SL(...this.filter((e) => !values.includes(e))),
);
}

withTrim() {
return Object.freeze(new SL(...super.map((e) => e.trim())));
return freezeIfImmutable(this, new SL(...super.map((e) => e.trim())));
}

withPrefix(prefix = '') {
return Object.freeze(new SL(...super.map((e) => `${prefix}${e}`)));
return freezeIfImmutable(
this,
new SL(...super.map((e) => `${prefix}${e}`)),
);
}

withSuffix(suffix = '') {
return Object.freeze(new SL(...super.map((e) => `${e}${suffix}`)));
return freezeIfImmutable(
this,
new SL(...super.map((e) => `${e}${suffix}`)),
);
}

withDerivatedSuffix(chars = '') {
return Object.freeze(
return freezeIfImmutable(
this,
new SL(
...super.flatMap((t) => [
t,
Expand All @@ -77,7 +112,8 @@ export class SL extends Array {
}

withDerivatedPrefix(chars = '') {
return Object.freeze(
return freezeIfImmutable(
this,
new SL(
...super.flatMap((t) => [
t,
Expand All @@ -90,13 +126,15 @@ export class SL extends Array {
}

withReplace(string, replacement = '') {
return Object.freeze(
return freezeIfImmutable(
this,
new SL(...super.map((e) => e.replace(string, replacement))),
);
}

withReplaceAll(string, replacement = '') {
return Object.freeze(
return freezeIfImmutable(
this,
new SL(...super.map((e) => e.replaceAll(string, replacement))),
);
}
Expand All @@ -113,7 +151,7 @@ export class SL extends Array {

// Get the native array
mutable() {
return Array.from(this.values());
return Array.from(this);
}

// Methods returning the native array
Expand Down Expand Up @@ -168,17 +206,4 @@ export const ARRAY_IN_PLACE_MUTATION = Object.freeze({
splice: 'splice',
});

Object.values(ARRAY_IN_PLACE_MUTATION).forEach((el) => {
SL.prototype[el] = function () {
/* c8 ignore start */
if (
typeof window === 'undefined' &&
process?.env?.NODE_ENV !== 'production' &&
process?.env?.NODE_ENV !== 'test'
) {
console && console.debug(`Array method ${el} is not supported`);
}
/* c8 ignore stop */
return Array.prototype[el].apply(this.mutable(), arguments);
};
});
export class SLS extends SL {}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 7e2c31f

Please sign in to comment.