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

TypeScript: add types for the array methods (map, filter, etc) #3

Merged
merged 12 commits into from
Jan 11, 2023
5 changes: 5 additions & 0 deletions .changeset/tame-guests-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"deepsignal": patch
---

TypeScript: add types for the array methods (map, filter, etc).
4 changes: 3 additions & 1 deletion .github/workflows/compressed-size.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: compressed-size
name: Compressed Size

on: pull_request

jobs:
build:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml → .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI
name: Tests

on: pull_request

Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/types.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Types

on: pull_request

jobs:
types:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18

- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
with:
version: 7
run_install: false

- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"

- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install

- name: Check types
run: pnpm ci:types
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"test": "cross-env COVERAGE=true karma start karma.conf.js --single-run",
"test:minify": "cross-env COVERAGE=true MINIFY=true karma start karma.conf.js --single-run",
"test:watch": "karma start karma.conf.js --no-single-run",
"test:types": "pnpm tsc packages/deepsignal/core/test/types.ts --noEmit",
"ci:build": "pnpm build",
"ci:test": "pnpm lint && pnpm test"
"ci:test": "pnpm lint && pnpm test",
"ci:types": "pnpm tsc packages/deepsignal/core/test/types.ts --noEmit || exit 1"
},
"authors": [
"@luisherranz"
Expand Down
200 changes: 150 additions & 50 deletions packages/deepsignal/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,13 @@ const arrayToArrayOfSignals = new WeakMap();
const rg = /^\$\$?/;
let peeking = false;

type DeepSignalObject<T extends object> = {
[P in keyof T & string as `$${P}`]?: Signal<T[P]>;
} & {
[P in keyof T]: T[P] extends Array<unknown>
? DeepSignalArray<T[P]>
: T[P] extends object
? DeepSignalObject<T[P]>
: T[P];
};

type ArrayType<T> = T extends Array<infer I> ? I : T;
type DeepSignalArray<T> = Array<ArrayType<T>> & {
[key: number]: DeepSignal<ArrayType<T>>;
$?: { [key: number]: Signal<ArrayType<T>> };
$length?: Signal<number>;
};

export type DeepSignal<T> = T extends Array<unknown>
? DeepSignalArray<T>
: T extends object
? DeepSignalObject<T>
: T;

export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>;

export const deepSignal = <T extends object>(obj: T): DeepSignal<T> => {
if (!shouldProxy(obj)) throw new Error("This object can't be observed.");
if (!objToProxy.has(obj))
objToProxy.set(obj, new Proxy(obj, objectHandlers) as DeepSignal<T>);
return objToProxy.get(obj);
};

type FilterSignals<K> = K extends `$${infer P}` ? never : K;
type RevertDeepSignalObject<T> = Pick<T, FilterSignals<keyof T>>;
type RevertDeepSignalArray<T> = Omit<T, "$" | "$length">;

type RevertDeepSignal<T> = T extends Array<unknown>
? RevertDeepSignalArray<T>
: T extends object
? RevertDeepSignalObject<T>
: T;

export const peek = <
T extends DeepSignalObject<object>,
K extends keyof RevertDeepSignalObject<T>
Expand Down Expand Up @@ -130,21 +95,6 @@ const arrayHandlers = {
},
};

type WellKnownSymbols =
| "asyncIterator"
| "hasInstance"
| "isConcatSpreadable"
| "iterator"
| "match"
| "matchAll"
| "replace"
| "search"
| "species"
| "split"
| "toPrimitive"
| "toStringTag"
| "unscopables";

const wellKnownSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => Symbol[key as WellKnownSymbols])
Expand All @@ -159,3 +109,153 @@ const shouldProxy = (val: any): boolean => {
(globalThis as any)[val.constructor.name] === val.constructor;
return !isBuiltIn || supported.has(val.constructor);
};

/** TYPES **/

type DeepSignalObject<T extends object> = {
[P in keyof T & string as `$${P}`]?: Signal<T[P]>;
} & {
[P in keyof T]: T[P] extends Array<unknown>
? DeepSignalArray<T[P]>
: T[P] extends object
? DeepSignalObject<T[P]>
: T[P];
};

/** @ts-expect-error **/
interface DeepArray<T> extends Array<T> {
map: <U>(
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => U,
thisArg?: any
) => U[];
forEach: (
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => void,
thisArg?: any
) => void;
concat(...items: ConcatArray<T>[]): DeepSignalArray<T[]>;
concat(...items: (T | ConcatArray<T>)[]): DeepSignalArray<T[]>;
reverse(): DeepSignalArray<T[]>;
shift(): DeepSignal<T> | undefined;
slice(start?: number, end?: number): DeepSignalArray<T[]>;
splice(start: number, deleteCount?: number): DeepSignalArray<T[]>;
splice(
start: number,
deleteCount: number,
...items: T[]
): DeepSignalArray<T[]>;
filter<S extends T>(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => value is DeepSignal<S>,
thisArg?: any
): DeepSignalArray<S[]>;
filter(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => unknown,
thisArg?: any
): DeepSignalArray<T[]>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduce<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduceRight<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
}
type ArrayType<T> = T extends Array<infer I> ? I : T;
type DeepSignalArray<T> = DeepArray<ArrayType<T>> & {
[key: number]: DeepSignal<ArrayType<T>>;
$?: { [key: number]: Signal<ArrayType<T>> };
$length?: Signal<number>;
};

export type DeepSignal<T> = T extends Array<unknown>
? DeepSignalArray<T>
: T extends object
? DeepSignalObject<T>
: T;

export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>;

type FilterSignals<K> = K extends `$${infer P}` ? never : K;
type RevertDeepSignalObject<T> = Pick<T, FilterSignals<keyof T>>;
type RevertDeepSignalArray<T> = Omit<T, "$" | "$length">;

type RevertDeepSignal<T> = T extends Array<unknown>
? RevertDeepSignalArray<T>
: T extends object
? RevertDeepSignalObject<T>
: T;

type WellKnownSymbols =
| "asyncIterator"
| "hasInstance"
| "isConcatSpreadable"
| "iterator"
| "match"
| "matchAll"
| "replace"
| "search"
| "species"
| "split"
| "toPrimitive"
| "toStringTag"
| "unscopables";
6 changes: 6 additions & 0 deletions packages/deepsignal/core/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ describe("deepsignal/core", () => {
store.switch = "b";
expect(store.aOrB.$data!.value).to.equal("b");
});

it("should return signals from array iterators", () => {
const store = deepSignal([{ a: 1 }, { a: 2 }]);
const signals = store.map(item => item.$a!.value);
expect(signals).to.deep.equal([1, 2]);
});
});

describe("set", () => {
Expand Down
42 changes: 42 additions & 0 deletions packages/deepsignal/core/test/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Signal } from "@preact/signals-core";
import { deepSignal, peek } from "../src";

// Arrays.
const array = deepSignal([{ a: 1 }, { a: 2 }]);
const a1: number = array.length;
const a2: Signal<number> = array.$length!;
const a3: number = peek(array, "length");
const a4: { a: number } = array[0];
const a5: Signal<{ a: number }> = array.$![0];
const a6: { a: number } = peek(array, 0);
const a7: number = array[0].a;
const a8: Signal<number> = array[0].$a!;
const a9: number = peek(array, 0).a;
const a10: number[] = array.map(item => item.a);
const a11: Signal<number>[] = array.map(item => item.$a!);
const a12 = array.forEach(item => item.$a);
const a13: Signal<number> = array.concat([{ a: 3 }])[0].$a!;
const a14: Signal<number> = array.concat({ a: 3 })[0].$a!;
const a15: Signal<number> = array.reverse()[0].$a!;
const a16: Signal<number> = array.shift()!.$a!;
const a17: Signal<number> = array.slice(0, 1)[0].$a!;
const a18: Signal<number> = array.sort()[0].$a!;
const a19: Signal<number> = array.splice(0, 1)[0].$a!;
const a20: Signal<number> = array.splice(0, 1, { a: 3 })[0].$a!;
const a21: Signal<number> = array.filter(item => !item.$a!)[0].$a!;
const a22: Signal<number> = array.reduce((prev, curr) =>
prev.$a!.value > curr.$a!.value ? prev : curr
).$a!;
const a23: number = array.reduce((prev, curr) => prev + curr.$a!.value, 0);
const a24: Signal<number> = array.reduce(
(prev, curr) => (prev.$a!.value > curr.$a!.value ? prev : curr),
array[0]
).$a!;
const a25: Signal<number> = array.reduceRight((prev, curr) =>
prev.$a!.value > curr.$a!.value ? prev : curr
).$a!;
const a26: number = array.reduceRight((prev, curr) => prev + curr.$a!.value, 0);
const a27: Signal<number> = array.reduceRight(
(prev, curr) => (prev.$a!.value > curr.$a!.value ? prev : curr),
{ a: 3 }
).$a!;