Skip to content

Commit 6ce496b

Browse files
authored
Merge pull request #95 from builder-group/94-new-feature-types
[Experimental] New Feature Types
2 parents d57197a + d0cb107 commit 6ce496b

File tree

7 files changed

+156
-93
lines changed

7 files changed

+156
-93
lines changed

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ A collection of open source libraries maintained by [builder.group](https://buil
1515

1616
## 📦 Packages
1717

18-
| Package | Description | NPM Package |
19-
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
18+
| Package | Description | NPM Package |
19+
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
2020
| [cli](https://github.com/builder-group/community/blob/develop/packages/cli) | Straightforward CLI to bundle Typescript libraries with presets, powered by Rollup and Esbuild | [`@blgc/cli`](https://www.npmjs.com/package/@blgc/cli) |
2121
| [config](https://github.com/builder-group/community/blob/develop/packages/config) | Collection of ESLint, Vite, and Typescript configurations | [`@blgc/config`](https://www.npmjs.com/package/@blgc/config) |
2222
| [elevenlabs-client](https://github.com/builder-group/community/blob/develop/packages/elevenlabs-client) | Typesafe and straightforward fetch client for interacting with the ElevenLabs API using feature-fetch | [`elevenlabs-client`](https://www.npmjs.com/package/elevenlabs-client) |
@@ -95,16 +95,17 @@ To switch between modes:
9595

9696
The `@blgc/types` package provides crucial TypeScript type definitions to ensure full type safety for feature-based libraries. When listed as a `devDependency`, these types are excluded from the final NPM package, resulting in broken type checks and missing autocompletions in projects consuming these libraries. By adding it as a `dependency`, we ensure that the type definitions are bundled and accessible to downstream projects, maintaining a seamless developer experience.
9797

98-
### Why we use the "wrapper pattern" `withLogger(withStorage(withUndo(createState(0))))` instead of a declarative API?
98+
### Why do we use the "wrapper pattern" (`withLogger(withStorage(withUndo(createState(0))))`) instead of a declarative API?
99+
100+
While declarative APIs like the following offer [better developer experience (DX)](https://www.reddit.com/r/reactjs/comments/1huxvci/i_built_a_bloated_state_manager_then_i_fixed_it/):
99101

100-
While a more declarative API like this has [indeed a better DX](https://www.reddit.com/r/reactjs/comments/1huxvci/i_built_a_bloated_state_manager_then_i_fixed_it/):
101102
```ts
102103
createState({
103-
defaultValue: 0,
104-
features: [withUndo(), withStorage(), withLogger()]
105-
})
104+
defaultValue: 0,
105+
features: [withUndo(), withStorage(), withLogger()]
106+
});
106107
```
107108

108-
Currently, we use the "wrapper pattern" because it provides better TypeScript type inference. Each wrapper function transforms the state's type, and these transformations need to build on top of each other in a specific order. While theoretically possible with a features array, maintaining proper type inference becomes complex.
109+
We currently use the "wrapper pattern" because it ensures better TypeScript type inference. Each wrapper function modifies the state's type in a specific sequence, which is harder to achieve reliably with a feature array.
109110

110-
We're [actively exploring solutions](https://github.com/builder-group/community/blob/develop/packages/feature-state/src/apply-features.ts) to support both patterns, as we recognize the benefits of a more declarative API. If you have suggestions or want to experiment with alternative approaches, feel free to contribute :)
111+
We're [actively exploring solutions](https://github.com/builder-group/community/blob/develop/packages/feature-state/src/_experimental) to support both patterns, combining the type safety of the wrapper pattern with the simplicity of declarative APIs. Contributions and ideas are always welcome :)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { TEnforceFeatureConstraint, TFeatureDefinition } from '@blgc/types/features';
2+
import { describe, it } from 'vitest';
3+
import { createState } from '../create-state';
4+
import { TStorageInterface } from '../features';
5+
import { TMultiUndoFeature, TPersistFeature, TState, TUndoFeature } from '../types';
6+
import { applyFeatures } from './apply-features';
7+
8+
describe('applyFeatures function', () => {
9+
it('should have correct types', () => {
10+
const state1 = applyFeatures(createState(0), withUndo(), withMultiUndo());
11+
const state2 = applyFeatures(createState(0), withUndo());
12+
const state3 = applyFeatures(
13+
createState(0),
14+
withUndo(),
15+
withMultiUndo(),
16+
withStorage(null as any, 'test')
17+
);
18+
const state4 = withMultiUndo()(withUndo()(createState(0)));
19+
});
20+
});
21+
22+
export function withMultiUndo() {
23+
return <GValue, GFeatures extends TFeatureDefinition[]>(
24+
state: TEnforceFeatureConstraint<TState<GValue, GFeatures>, TState<GValue, GFeatures>, ['undo']>
25+
): TState<GValue, [TMultiUndoFeature, ...GFeatures]> => {
26+
return null as any;
27+
};
28+
}
29+
30+
export function withStorage<GStorageValue>(storage: TStorageInterface<GStorageValue>, key: string) {
31+
return <GValue, GFeatures extends TFeatureDefinition[]>(
32+
state: TEnforceFeatureConstraint<TState<GValue, GFeatures>, TState<GValue, GFeatures>, []>
33+
): TState<GValue, [TPersistFeature, ...GFeatures]> => {
34+
return null as any;
35+
};
36+
}
37+
38+
export function withUndo(historyLimit = 50) {
39+
return <GValue, GFeatures extends TFeatureDefinition[]>(
40+
state: TEnforceFeatureConstraint<TState<GValue, GFeatures>, TState<GValue, GFeatures>, []>
41+
): TState<GValue, [TUndoFeature<GValue>, ...GFeatures]> => {
42+
return null as any;
43+
};
44+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { TFeatureDefinition } from '@blgc/types/features';
2+
import { TState } from '../types';
3+
4+
// We tried two approaches with array parameters that didn't work due to TypeScript limitations:
5+
//
6+
// 1. Using array of feature functions with return type constraint:
7+
// function applyFeatures<
8+
// GValue,
9+
// GInitialFeatures extends TFeatureDefinition[],
10+
// GF0 extends TFeatureDefinition,
11+
// GF1 extends TFeatureDefinition
12+
// >(
13+
// initialState: TState<GValue, GInitialFeatures>,
14+
// [(state: TState<GValue, GInitialFeatures>) => GF0,
15+
// (state: TState<GValue, [GF0, ...GInitialFeatures]>) => GF1]
16+
// ): TState<GValue, [GF1, GF0, ...GInitialFeatures]>
17+
// Issue: Generic type inference fails (see: https://stackoverflow.com/q/54955340)
18+
// Example: TUndoFeature<GValue> becomes TUndoFeature<unknown>
19+
// When we tried working around this by using TState<GValue, [any, ...GInitialFeatures]>,
20+
// it fixed type inference issue, but at the cost of type safety at a different place:
21+
// TypeScript could no longer detect missing feature dependencies.
22+
// Thus e.g. applying withMultiUndo without its required withUndo feature was possible.
23+
//
24+
// 2. Using function type constraints:
25+
// function applyFeatures<
26+
// GValue,
27+
// GInitialFeatures extends TFeatureDefinition[],
28+
// GF0 extends (state: TState<GValue, GInitialFeatures>) => TFeatureDefinition,
29+
// GF1 extends (state: TState<GValue, [ReturnType<GF0>, ...GInitialFeatures]>) => TFeatureDefinition
30+
// >(
31+
// initialState: TState<GValue, GInitialFeatures>,
32+
// [GF0, GF1]
33+
// ): TState<GValue, [ReturnType<GF1>, ReturnType<GF0>, ...GInitialFeatures]>
34+
// Issue: ReturnType<GF0> loses generic type information (see: https://stackoverflow.com/q/64948037)
35+
// Example: TUndoFeature<GValue> becomes TUndoFeature<unknown>
36+
//
37+
// Current working solution uses separate function parameters to preserve type inference:
38+
// function applyFeatures<GValue, ...>(
39+
// initialState: TState<GValue, GInitialFeatures>,
40+
// f1: (state: TState<GValue, GInitialFeatures>) => GF0,
41+
// f2: (state: TState<GValue, [GF0, ...GInitialFeatures]>) => GF1
42+
// ): TState<GValue, [GF1, GF0, ...GInitialFeatures]>
43+
44+
// Overload for one feature
45+
export function applyFeatures<
46+
GValue,
47+
GInitialFeatures extends TFeatureDefinition[],
48+
GF0 extends TFeatureDefinition
49+
>(
50+
initialState: TState<GValue, GInitialFeatures>,
51+
f1: (state: TState<GValue, GInitialFeatures>) => TState<GValue, [GF0, ...GInitialFeatures]>
52+
): TState<GValue, [GF0, ...GInitialFeatures]>;
53+
54+
// Overload for two features
55+
export function applyFeatures<
56+
GValue,
57+
GInitialFeatures extends TFeatureDefinition[],
58+
GF0 extends TFeatureDefinition,
59+
GF1 extends TFeatureDefinition
60+
>(
61+
initialState: TState<GValue, GInitialFeatures>,
62+
f1: (state: TState<GValue, GInitialFeatures>) => TState<GValue, [GF0, ...GInitialFeatures]>,
63+
f2: (
64+
state: TState<GValue, [GF0, ...GInitialFeatures]>
65+
) => TState<GValue, [GF1, GF0, ...GInitialFeatures]>
66+
): TState<GValue, [GF1, GF0, ...GInitialFeatures]>;
67+
68+
// Overload for three features
69+
export function applyFeatures<
70+
GValue,
71+
GInitialFeatures extends TFeatureDefinition[],
72+
GF0 extends TFeatureDefinition,
73+
GF1 extends TFeatureDefinition,
74+
GF2 extends TFeatureDefinition
75+
>(
76+
initialState: TState<GValue, GInitialFeatures>,
77+
f1: (state: TState<GValue, GInitialFeatures>) => TState<GValue, [GF0, ...GInitialFeatures]>,
78+
f2: (
79+
state: TState<GValue, [GF0, ...GInitialFeatures]>
80+
) => TState<GValue, [GF1, GF0, ...GInitialFeatures]>,
81+
f3: (
82+
state: TState<GValue, [GF1, GF0, ...GInitialFeatures]>
83+
) => TState<GValue, [GF2, GF1, GF0, ...GInitialFeatures]>
84+
): TState<GValue, [GF2, GF1, GF0, ...GInitialFeatures]>;
85+
86+
// Implementation
87+
export function applyFeatures<
88+
GValue,
89+
GInitialFeatures extends TFeatureDefinition[],
90+
GF0 extends TFeatureDefinition,
91+
GF1 extends TFeatureDefinition,
92+
GF2 extends TFeatureDefinition
93+
>(
94+
initialState: TState<GValue, GInitialFeatures>,
95+
f1: (state: TState<GValue, GInitialFeatures>) => GF0,
96+
f2?: (state: TState<GValue, [GF0, ...GInitialFeatures]>) => GF1,
97+
f3?: (state: TState<GValue, [GF1, GF0, ...GInitialFeatures]>) => GF2
98+
): TState<GValue, any> {
99+
// TODO: Implement
100+
return null as any;
101+
}

packages/feature-state/src/apply-features.test.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

packages/feature-state/src/apply-features.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

packages/feature-state/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export * from './apply-features';
21
export * from './create-state';
32
export * from './features';
43
export * from './is-state-with-features';

packages/openapi-ts-router/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
```ts
4848
import { Router } from 'express';
4949
import { createExpressOpenApiRouter } from 'openapi-ts-router';
50-
import * as z from 'zod';
5150
import { zValidator } from 'validation-adapters/zod';
51+
import * as z from 'zod';
5252
import { paths } from './gen/v1'; // OpenAPI-generated types
5353
import { PetSchema } from './schemas'; // Custom reusable schema for validation
5454

0 commit comments

Comments
 (0)