Skip to content
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
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ A collection of open source libraries maintained by [builder.group](https://buil

## 📦 Packages

| Package | Description | NPM Package |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| Package | Description | NPM Package |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| [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) |
| [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) |
| [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) |
Expand Down Expand Up @@ -95,16 +95,17 @@ To switch between modes:

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.

### Why we use the "wrapper pattern" `withLogger(withStorage(withUndo(createState(0))))` instead of a declarative API?
### Why do we use the "wrapper pattern" (`withLogger(withStorage(withUndo(createState(0))))`) instead of a declarative API?

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/):

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/):
```ts
createState({
defaultValue: 0,
features: [withUndo(), withStorage(), withLogger()]
})
defaultValue: 0,
features: [withUndo(), withStorage(), withLogger()]
});
```

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.
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.

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 :)
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 :)
44 changes: 44 additions & 0 deletions packages/feature-state/src/_experimental/apply-features.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TEnforceFeatureConstraint, TFeatureDefinition } from '@blgc/types/features';
import { describe, it } from 'vitest';
import { createState } from '../create-state';
import { TStorageInterface } from '../features';
import { TMultiUndoFeature, TPersistFeature, TState, TUndoFeature } from '../types';
import { applyFeatures } from './apply-features';

describe('applyFeatures function', () => {
it('should have correct types', () => {
const state1 = applyFeatures(createState(0), withUndo(), withMultiUndo());
const state2 = applyFeatures(createState(0), withUndo());
const state3 = applyFeatures(
createState(0),
withUndo(),
withMultiUndo(),
withStorage(null as any, 'test')
);
const state4 = withMultiUndo()(withUndo()(createState(0)));
});
});

export function withMultiUndo() {
return <GValue, GFeatures extends TFeatureDefinition[]>(
state: TEnforceFeatureConstraint<TState<GValue, GFeatures>, TState<GValue, GFeatures>, ['undo']>
): TState<GValue, [TMultiUndoFeature, ...GFeatures]> => {
return null as any;
};
}

export function withStorage<GStorageValue>(storage: TStorageInterface<GStorageValue>, key: string) {
return <GValue, GFeatures extends TFeatureDefinition[]>(
state: TEnforceFeatureConstraint<TState<GValue, GFeatures>, TState<GValue, GFeatures>, []>
): TState<GValue, [TPersistFeature, ...GFeatures]> => {
return null as any;
};
}

export function withUndo(historyLimit = 50) {
return <GValue, GFeatures extends TFeatureDefinition[]>(
state: TEnforceFeatureConstraint<TState<GValue, GFeatures>, TState<GValue, GFeatures>, []>
): TState<GValue, [TUndoFeature<GValue>, ...GFeatures]> => {
return null as any;
};
}
101 changes: 101 additions & 0 deletions packages/feature-state/src/_experimental/apply-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { TFeatureDefinition } from '@blgc/types/features';
import { TState } from '../types';

// We tried two approaches with array parameters that didn't work due to TypeScript limitations:
//
// 1. Using array of feature functions with return type constraint:
// function applyFeatures<
// GValue,
// GInitialFeatures extends TFeatureDefinition[],
// GF0 extends TFeatureDefinition,
// GF1 extends TFeatureDefinition
// >(
// initialState: TState<GValue, GInitialFeatures>,
// [(state: TState<GValue, GInitialFeatures>) => GF0,
// (state: TState<GValue, [GF0, ...GInitialFeatures]>) => GF1]
// ): TState<GValue, [GF1, GF0, ...GInitialFeatures]>
// Issue: Generic type inference fails (see: https://stackoverflow.com/q/54955340)
// Example: TUndoFeature<GValue> becomes TUndoFeature<unknown>
// When we tried working around this by using TState<GValue, [any, ...GInitialFeatures]>,
// it fixed type inference issue, but at the cost of type safety at a different place:
// TypeScript could no longer detect missing feature dependencies.
// Thus e.g. applying withMultiUndo without its required withUndo feature was possible.
//
// 2. Using function type constraints:
// function applyFeatures<
// GValue,
// GInitialFeatures extends TFeatureDefinition[],
// GF0 extends (state: TState<GValue, GInitialFeatures>) => TFeatureDefinition,
// GF1 extends (state: TState<GValue, [ReturnType<GF0>, ...GInitialFeatures]>) => TFeatureDefinition
// >(
// initialState: TState<GValue, GInitialFeatures>,
// [GF0, GF1]
// ): TState<GValue, [ReturnType<GF1>, ReturnType<GF0>, ...GInitialFeatures]>
// Issue: ReturnType<GF0> loses generic type information (see: https://stackoverflow.com/q/64948037)
// Example: TUndoFeature<GValue> becomes TUndoFeature<unknown>
//
// Current working solution uses separate function parameters to preserve type inference:
// function applyFeatures<GValue, ...>(
// initialState: TState<GValue, GInitialFeatures>,
// f1: (state: TState<GValue, GInitialFeatures>) => GF0,
// f2: (state: TState<GValue, [GF0, ...GInitialFeatures]>) => GF1
// ): TState<GValue, [GF1, GF0, ...GInitialFeatures]>

// Overload for one feature
export function applyFeatures<
GValue,
GInitialFeatures extends TFeatureDefinition[],
GF0 extends TFeatureDefinition
>(
initialState: TState<GValue, GInitialFeatures>,
f1: (state: TState<GValue, GInitialFeatures>) => TState<GValue, [GF0, ...GInitialFeatures]>
): TState<GValue, [GF0, ...GInitialFeatures]>;

// Overload for two features
export function applyFeatures<
GValue,
GInitialFeatures extends TFeatureDefinition[],
GF0 extends TFeatureDefinition,
GF1 extends TFeatureDefinition
>(
initialState: TState<GValue, GInitialFeatures>,
f1: (state: TState<GValue, GInitialFeatures>) => TState<GValue, [GF0, ...GInitialFeatures]>,
f2: (
state: TState<GValue, [GF0, ...GInitialFeatures]>
) => TState<GValue, [GF1, GF0, ...GInitialFeatures]>
): TState<GValue, [GF1, GF0, ...GInitialFeatures]>;

// Overload for three features
export function applyFeatures<
GValue,
GInitialFeatures extends TFeatureDefinition[],
GF0 extends TFeatureDefinition,
GF1 extends TFeatureDefinition,
GF2 extends TFeatureDefinition
>(
initialState: TState<GValue, GInitialFeatures>,
f1: (state: TState<GValue, GInitialFeatures>) => TState<GValue, [GF0, ...GInitialFeatures]>,
f2: (
state: TState<GValue, [GF0, ...GInitialFeatures]>
) => TState<GValue, [GF1, GF0, ...GInitialFeatures]>,
f3: (
state: TState<GValue, [GF1, GF0, ...GInitialFeatures]>
) => TState<GValue, [GF2, GF1, GF0, ...GInitialFeatures]>
): TState<GValue, [GF2, GF1, GF0, ...GInitialFeatures]>;

// Implementation
export function applyFeatures<
GValue,
GInitialFeatures extends TFeatureDefinition[],
GF0 extends TFeatureDefinition,
GF1 extends TFeatureDefinition,
GF2 extends TFeatureDefinition
>(
initialState: TState<GValue, GInitialFeatures>,
f1: (state: TState<GValue, GInitialFeatures>) => GF0,
f2?: (state: TState<GValue, [GF0, ...GInitialFeatures]>) => GF1,
f3?: (state: TState<GValue, [GF1, GF0, ...GInitialFeatures]>) => GF2
): TState<GValue, any> {
// TODO: Implement
return null as any;
}
18 changes: 0 additions & 18 deletions packages/feature-state/src/apply-features.test.ts

This file was deleted.

64 changes: 0 additions & 64 deletions packages/feature-state/src/apply-features.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/feature-state/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './apply-features';
export * from './create-state';
export * from './features';
export * from './is-state-with-features';
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
```ts
import { Router } from 'express';
import { createExpressOpenApiRouter } from 'openapi-ts-router';
import * as z from 'zod';
import { zValidator } from 'validation-adapters/zod';
import * as z from 'zod';
import { paths } from './gen/v1'; // OpenAPI-generated types
import { PetSchema } from './schemas'; // Custom reusable schema for validation

Expand Down
Loading