Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stricter Types with HotScript #75

Merged
merged 21 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-bikes-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"moderndash": minor
---

`set` | Path autocomplete & correct return types
5 changes: 5 additions & 0 deletions .changeset/mighty-pianos-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"moderndash": patch
---

`set` | Fix path validation
5 changes: 5 additions & 0 deletions .changeset/polite-lobsters-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"moderndash": minor
---

`FlatKeys` | Correct return types
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
module.exports = {
root: true,
extends: ["dewald"]
};
};
3,026 changes: 950 additions & 2,076 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
"vitest": "0.30.1",
"@vitest/coverage-c8": "0.30.1",
"@vitest/ui": "0.30.1",
"vite": "4.2.1"
"vite": "4.2.1",
"hotscript": "1.0.11"
},
"overrides": {
"tsup": {
"rollup": "3.20.4"
}
}
}
}
7 changes: 6 additions & 1 deletion package/src/number/sum.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@

import type { Tuples, Call } from "hotscript";

/**
* Calculates the sum of an array of numbers.
*
Expand All @@ -9,7 +12,9 @@
* @returns The sum of the input array
*/

export function sum(numbers: readonly number[]): number {
export function sum(numbers: number[]): number;
export function sum<TNum extends readonly number[]>(numbers: TNum): Call<Tuples.Sum, TNum>;
export function sum<TNum extends readonly number[]>(numbers: TNum): Call<Tuples.Sum, TNum> | number {
if (numbers.length === 0)
return NaN;
return numbers.reduce((total, current) => total + current, 0);
Expand Down
8 changes: 6 additions & 2 deletions package/src/object/flatKeys.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { PlainObject } from "@type/PlainObject.js";
import type { Call, Objects } from "hotscript";

import { isPlainObject } from "@validate/isPlainObject.js";

type StringIfNever<Type> = [Type] extends [never] ? string : Type;
type Paths<TObj> = StringIfNever<Call<Objects.AllPaths, TObj>>;

/**
* Flattens an object into a single level object.
*
Expand All @@ -15,7 +19,7 @@ import { isPlainObject } from "@validate/isPlainObject.js";
* @returns A new object with flattened keys.
*/

export function flatKeys<TObj extends PlainObject>(obj: TObj): Record<string, unknown> {
export function flatKeys<TObj extends PlainObject>(obj: TObj): Record<Paths<TObj>, unknown> {
const flatObject: Record<string, unknown> = {};

for (const [key, value] of Object.entries(obj)) {
Expand All @@ -39,4 +43,4 @@ function addToResult(prefix: string, value: unknown, flatObject: Record<string,
} else {
flatObject[prefix] = value;
}
}
}
15 changes: 11 additions & 4 deletions package/src/object/set.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { PlainObject } from "@type/PlainObject.js";
import type { Call, Objects } from "hotscript";

import { isPlainObject } from "@validate/isPlainObject.js";

const validPathRegex = /^(?:[^.[\]]+(?:\[\d+])*(?:\.|\[\d+]))+(?:[^.[\]]+(?:\[\d+])*)+$/;
const validPathRegex = /^[^.[\]]+(?:\.[^.[\]]+)*(?:\[\d+])*(?:\.[^.[\]]+(?:\[\d+])*)*$/;
const pathSplitRegex = /\.|(?=\[)/g;
const matchBracketsRegex = /[[\]]/g;

// eslint-disable-next-line @typescript-eslint/ban-types
type Paths<TObj> = Call<Objects.AllPaths, TObj> | string & {};
type UpdateObj<TObj extends PlainObject, TPath extends string, TVal> = Call<Objects.Update<TPath, TVal>, TObj>;

/**
* Sets the value at path of object. If a portion of path doesn’t exist, it’s created.
*
Expand All @@ -30,14 +35,16 @@ const matchBracketsRegex = /[[\]]/g;
* @param path The path of the property to set.
* @param value The value to set.
* @template TObj The type of the object.
* @template TPath The type of the object path.
* @template TVal The type of the value to set.
* @returns The modified object.
*/

export function set(obj: PlainObject, path: string, value: unknown): PlainObject {
export function set<TObj extends PlainObject, TPath extends Paths<TObj>, TVal>(obj: TObj, path: TPath, value: TVal): UpdateObj<TObj, TPath, TVal> {
if (!validPathRegex.test(path))
throw new Error("Invalid path, look at the examples for the correct format.");

const pathParts = path.split(pathSplitRegex);
const pathParts = (path as string).split(pathSplitRegex);
let currentObj: PlainObject = obj;
for (let index = 0; index < pathParts.length; index++) {
const key = pathParts[index].replace(matchBracketsRegex, "");
Expand All @@ -58,5 +65,5 @@ export function set(obj: PlainObject, path: string, value: unknown): PlainObject
currentObj = currentObj[key] as PlainObject;
}

return obj;
return obj as UpdateObj<TObj, TPath, TVal>;
}
4 changes: 2 additions & 2 deletions package/test/crypto/randomFloat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ test("can return the upper and lower bounds", () => {
expect(results).toContain(max);
});

test("average of 100000 random numbers should be close to the middle", () => {
test("average of 200000 random numbers should be close to the middle", () => {
const min = 0;
const max = 1;
const iterations = 100000;
const iterations = 200000;
let sum = 0;

for (let i = 0; i < iterations; i++) {
Expand Down
22 changes: 16 additions & 6 deletions package/test/object/flatKeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import type { PlainObject } from "@type/PlainObject.js";

import { flatKeys } from "@object/flatKeys.js";

test("correct flattened keys", () => {
const obj = { a: 1, b: { c: 2, d: { e: 3 } } };
expect(flatKeys(obj)).toEqual({ a: 1, "b.c": 2, "b.d.e": 3 });
});

test("correct flattened keys with arrays", () => {
const obj = { a: 1, b: { c: 2, d: [{ e: 3 }, { e: 4, f: { g: 5 } }] } };
expect(flatKeys(obj)).toEqual({ a: 1, "b.c": 2, "b.d[0].e": 3, "b.d[1].e": 4, "b.d[1].f.g": 5 });
test("correct flattened keys", () => {
const obj = { a: 1, b: { c: 2, d: { e: 3 } } };
const flat = flatKeys(obj);
const flatGeneric = flatKeys(obj as PlainObject);

expectTypeOf(flat).toEqualTypeOf<Record<"a" | "b" | "b.c" | "b.d" | "b.d.e", unknown>>();
expectTypeOf(flatGeneric).toEqualTypeOf<Record<string, unknown>>();

expect(flatKeys(obj)).toEqual({ a: 1, "b.c": 2, "b.d.e": 3 });
});

test("nested arrays", () => {
const obj = { a: [[1, 2], [3, 4]] };
expect(flatKeys(obj)).toEqual({ "a[0][0]": 1, "a[0][1]": 2, "a[1][0]": 3, "a[1][1]": 4 });
test("correct flattened keys with arrays", () => {
const obj = { a: 1, b: { c: 2, d: [{ e: 3 }, { e: 4, f: { g: 5 } }] } };
const flat = flatKeys(obj);
expectTypeOf(flat).toEqualTypeOf<Record<"a" | "b" | "b.c" | "b.d" | `b.d[${number}]` | `b.d[${number}].e` | `b.d[${number}].f` | `b.d[${number}].f.g`, unknown>>();
expect(flat).toEqual({ a: 1, "b.c": 2, "b.d[0].e": 3, "b.d[1].e": 4, "b.d[1].f.g": 5 });
});

test("simple array", () => {
Expand Down
20 changes: 14 additions & 6 deletions package/test/object/set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { set } from "@object/set.js";

test("set a value", () => {
const obj = { a: { b: 2 } };
set(obj, "a.c", 1);
const updatedObj = set(obj, "a.c", 1);

expectTypeOf(updatedObj).toEqualTypeOf<{ a: { b: number; c: number } }>();
expect(obj).toEqual({ a: { b: 2, c: 1 } });

const updatedObj2 = set(obj, "a.c.d", 1);
expectTypeOf(updatedObj2).toEqualTypeOf<{ a: { b: number; c: { d: number } } }>();
expect(obj).toEqual({ a: { b: 2, c: { d: 1 } } });
});

test("set a value with array path", () => {
Expand All @@ -18,11 +24,13 @@ test("set a value with array path", () => {
expect(obj).toEqual({ a: [{ c: 3 }] });
});

test("recognise number key", () => {
const obj = { a: 1 };
set(obj, "a.e0[0]", 1);
expect(obj).toEqual({ a: { e0: [1] } });
});
// TODO Waiting for hotscript fix
// test("recognize number key", () => {
// const obj = { a: 1 };
// const updatedObj = set(obj, "a[0]", 4);
// expectTypeOf(updatedObj).toEqualTypeOf<{ a: number[] }>();
// expect(obj).toEqual({ a: [4] });
// });

test("throw error on incorrect path format", () => {
const obj = { a: { b: 2 } };
Expand Down