Skip to content

Commit

Permalink
Refactored typings (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski authored Feb 6, 2024
1 parent 7b3f22a commit 00b1dfe
Show file tree
Hide file tree
Showing 29 changed files with 481 additions and 224 deletions.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Let's start by creating a field:
import { createField } from 'roqueform';

const field = createField();
// ⮕ Field<unknown, any>
// ⮕ Field<any>
```

A value can be set to and retrieved from the field:
Expand All @@ -85,7 +85,7 @@ Provide the initial value for a field:

```ts
const ageField = createField(42);
// ⮕ Field<unknown, number>
// ⮕ Field<number>

ageField.value;
// ⮕ 42
Expand All @@ -103,7 +103,7 @@ interface Universe {
}

const universeField = createField<Universe>();
// ⮕ Field<unknown, Universe | undefined>
// ⮕ Field<Universe | undefined>

universeField.value;
// ⮕ undefined
Expand All @@ -113,7 +113,7 @@ Retrieve a child field by its key:

```ts
const planetsField = universeField.at('planets');
// ⮕ Field<unknown, Planet[] | undefined>
// ⮕ Field<Planet[] | undefined>
```

`planetsField` is a child field, and it is linked to its parent `universeField`.
Expand All @@ -126,7 +126,7 @@ planetsField.parent;
// ⮕ universeField
```

Fields returned by the [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#at)
Fields returned by the [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#at)
method have a stable identity. This means that you can invoke `at` with the same key multiple times and the same field
instance is returned:

Expand All @@ -142,7 +142,7 @@ The child field has all the same functionality as its parent, so you can access

```ts
planetsField.at(0).at('name');
// ⮕ Field<unknown, string>
// ⮕ Field<string>
```

When a value is set to a child field, a parent field value is also updated. If parent field doesn't have a value yet,
Expand Down Expand Up @@ -186,14 +186,14 @@ const unsubscribe = planetsField.on('change:value', event => {
// ⮕ () => void
```

The [`Field.on`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#on) method
The [`Field.on`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#on) method
associates the event subscriber with an event type. All events that are dispatched onto fields have the share
[`Event`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Event.html).

Without plugins, fields can dispatch events with
[`change:value`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#on.on-2) type. This
[`change:value`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#on.on-2) type. This
event is dispatched when the field value is changed via
[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setValue).
[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue).

Plugins may dispatch their own events. Here's an example of the
[`change:errors`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.ErrorsPlugin.html#on.on-1) event
Expand Down Expand Up @@ -234,7 +234,7 @@ planetsField.on('*', event => {

# Transient updates

When you call [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setValue)
When you call [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue)
on a field then subscribers of its ancestors and its updated child fields are triggered. To manually control the update
propagation to fields ancestors, you can use transient updates.

Expand All @@ -261,7 +261,7 @@ avatarField.at('eyeColor').isTransient;
```

To propagate the transient value contained by the child field to its parent, use the
[`Field.propagate`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#propagate)
[`Field.propagate`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#propagate)
method:

```ts
Expand All @@ -271,7 +271,7 @@ avatarField.value;
// ⮕ { eyeColor: 'green' }
```

[`Field.setTransientValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setTransientValue)
[`Field.setTransientValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setTransientValue)
can be called multiple times, but only the most recent update is propagated to the parent field after the `propagate`
call.

Expand Down Expand Up @@ -308,12 +308,12 @@ planetsField.at(1).value;
updates field values.

- When the child field is accessed via
[`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#at) method for the
[`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#at) method for the
first time, its value is read from the value of the parent field using the
[`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.

- When a field value is updated via
[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setValue), then
[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue), then
the parent field value is updated with the value returned from the
[`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the
updated field has child fields, their values are updated with values returned from the
Expand Down Expand Up @@ -401,7 +401,7 @@ const planetField = createField(
{ name: 'Mars' },
injectElementPlugin
);
// ⮕ Field<{ element: Element | null }, { name: string }>
// ⮕ Field<{ name: string }, { element: Element | null }>

planetField.element;
// ⮕ null
Expand Down Expand Up @@ -475,7 +475,7 @@ To combine multiple plugins into a single function, use the
import { createField, composePlugins } from 'roqueform';

createField(['Mars'], composePlugins(plugin1, plugin2));
// ⮕ Field<…, string[]>
// ⮕ Field<string[], …>
```

# Errors plugin
Expand Down
4 changes: 2 additions & 2 deletions packages/annotations-plugin/src/main/annotationsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface AnnotationsPlugin<Annotations extends object> {
* @param subscriber The subscriber that would be triggered.
* @returns The callback to unsubscribe the subscriber.
*/
on(eventType: 'change:annotations', subscriber: Subscriber<PluginOf<this>, Annotations>): Unsubscribe;
on(eventType: 'change:annotations', subscriber: Subscriber<Annotations, PluginOf<this>>): Unsubscribe;
}

/**
Expand Down Expand Up @@ -97,7 +97,7 @@ function applyChanges(annotations: ReadonlyDict, patch: ReadonlyDict): ReadonlyD
}

function annotate(
field: Field<AnnotationsPlugin<ReadonlyDict>>,
field: Field<unknown, AnnotationsPlugin<ReadonlyDict>>,
patch: ReadonlyDict | ((annotations: ReadonlyDict) => ReadonlyDict),
applyPatch: (annotations: ReadonlyDict, patch: ReadonlyDict) => ReadonlyDict,
options: AnnotateOptions | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface ConstraintValidationPlugin {
/**
* Returns all invalid fields.
*/
getInvalidFields(): Field<PluginOf<this>>[];
getInvalidFields(): Field<any, PluginOf<this>>[];

/**
* Subscribes to {@link validity the validity} changes of this field or any of its descendants.
Expand All @@ -48,7 +48,7 @@ export interface ConstraintValidationPlugin {
* @returns The callback to unsubscribe the subscriber.
* @see {@link isInvalid}
*/
on(eventType: 'change:validity', subscriber: Subscriber<PluginOf<this>, ValidityState | null>): Unsubscribe;
on(eventType: 'change:validity', subscriber: Subscriber<ValidityState | null, PluginOf<this>>): Unsubscribe;
}

/**
Expand Down Expand Up @@ -118,7 +118,7 @@ export function constraintValidationPlugin(): PluginInjector<ConstraintValidatio
};
}

function applyValidity(field: Field<ConstraintValidationPlugin>): void {
function applyValidity(field: Field<unknown, ConstraintValidationPlugin>): void {
const prevValidity = field.validity;
const nextValidity = field.validatedElement !== null ? cloneValidity(field.validatedElement.validity) : null;

Expand All @@ -129,7 +129,7 @@ function applyValidity(field: Field<ConstraintValidationPlugin>): void {
}
}

function reportValidity(field: Field<ConstraintValidationPlugin>): boolean {
function reportValidity(field: Field<unknown, ConstraintValidationPlugin>): boolean {
if (field.children !== null) {
for (const child of field.children) {
if (!reportValidity(child)) {
Expand All @@ -141,9 +141,9 @@ function reportValidity(field: Field<ConstraintValidationPlugin>): boolean {
}

function getInvalidFields(
field: Field<ConstraintValidationPlugin>,
batch: Field<ConstraintValidationPlugin>[]
): Field<ConstraintValidationPlugin>[] {
field: Field<unknown, ConstraintValidationPlugin>,
batch: Field<unknown, ConstraintValidationPlugin>[]
): Field<unknown, ConstraintValidationPlugin>[] {
if (field.isInvalid) {
batch.push(field);
}
Expand Down
14 changes: 7 additions & 7 deletions packages/doubter-plugin/src/main/doubterPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import {
composePlugins,
errorsPlugin,
ErrorsPlugin,
FieldController,
Field,
PluginInjector,
Validation,
ValidationPlugin,
validationPlugin,
Validator,
} from 'roqueform';

export interface ValueShapePlugin {
export interface DoubterShapePlugin {
/**
* The shape that Doubter uses to validate {@link FieldController.value the field value}, or `null` if there's no
* The shape that Doubter uses to validate {@link roqueform!BareField.value the field value}, or `null` if there's no
* shape for this field.
*/
valueShape: Shape | null;
Expand All @@ -31,7 +31,7 @@ export interface ValueShapePlugin {
/**
* The plugin added to fields by the {@link doubterPlugin}.
*/
export type DoubterPlugin = ValidationPlugin<ParseOptions> & ErrorsPlugin<Issue> & ValueShapePlugin;
export type DoubterPlugin = ValidationPlugin<ParseOptions> & ErrorsPlugin<Issue> & DoubterShapePlugin;

/**
* Enhances fields with validation methods powered by [Doubter](https://github.com/smikhalevski/doubter#readme).
Expand All @@ -40,10 +40,10 @@ export type DoubterPlugin = ValidationPlugin<ParseOptions> & ErrorsPlugin<Issue>
* @template Value The root field value.
*/
export function doubterPlugin<Value>(shape: Shape<Value, any>): PluginInjector<DoubterPlugin, Value> {
return validationPlugin(composePlugins(errorsPlugin(concatErrors), valueShapePlugin(shape)), validator);
return validationPlugin(composePlugins(errorsPlugin(concatErrors), doubterShapePlugin(shape)), validator);
}

function valueShapePlugin(rootShape: Shape): PluginInjector<ValueShapePlugin> {
function doubterShapePlugin<Value>(rootShape: Shape<Value, any>): PluginInjector<DoubterShapePlugin, Value> {
return field => {
field.valueShape = field.parentField === null ? rootShape : field.parentField.valueShape?.at(field.key) || null;

Expand Down Expand Up @@ -92,7 +92,7 @@ function concatErrors(errors: readonly Issue[], error: Issue): readonly Issue[]
return errors.concat(error);
}

function prependPath(field: FieldController<any>, issue: Issue): Issue {
function prependPath(field: Field, issue: Issue): Issue {
for (; field.parentField !== null; field = field.parentField) {
(issue.path ||= []).unshift(field.key);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/doubter-plugin/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
*/

export { doubterPlugin } from './doubterPlugin';
export type { DoubterPlugin, ValueShapePlugin } from './doubterPlugin';
export type { DoubterPlugin, DoubterShapePlugin } from './doubterPlugin';
11 changes: 8 additions & 3 deletions packages/doubter-plugin/src/test/doubterPlugin.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { annotationsPlugin, AnnotationsPlugin } from '@roqueform/annotations-plugin';
import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin';
import * as d from 'doubter';
import { composePlugins, createField, Field } from 'roqueform';
import { expectType } from 'tsd';
import { createField, Field } from 'roqueform';
import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin';

const shape = d.object({ aaa: d.object({ bbb: d.string() }) });

expectType<Field<DoubterPlugin, { aaa: { bbb: string } }>>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape)));
expectType<Field<{ aaa: { bbb: string } }, DoubterPlugin>>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape)));

expectType<Field<{ aaa: { bbb: string } }, AnnotationsPlugin<{ xxx: string }> & DoubterPlugin>>(
createField({ aaa: { bbb: 'aaa' } }, composePlugins(doubterPlugin(shape), annotationsPlugin({ xxx: 'yyy' })))
);
14 changes: 7 additions & 7 deletions packages/react/src/main/FieldRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ import {
useReducer,
useRef,
} from 'react';
import { callOrGet, FieldController, ValueOf } from 'roqueform';
import { callOrGet, Field, ValueOf } from 'roqueform';

/**
* Properties of the {@link FieldRenderer} component.
*
* @template Field The rendered field.
*/
export interface FieldRendererProps<Field extends FieldController<any>> {
export interface FieldRendererProps<F extends Field> {
/**
* The field that triggers re-renders.
*/
field: Field;
field: F;

/**
* The render function that receive a rendered field as an argument.
*/
children: (field: Field) => ReactNode;
children: (field: F) => ReactNode;

/**
* If set to `true` then {@link FieldRenderer} is re-rendered whenever the {@link field} itself, its parent fields or
Expand All @@ -40,7 +40,7 @@ export interface FieldRendererProps<Field extends FieldController<any>> {
*
* @param value The new field value.
*/
onChange?: (value: ValueOf<Field>) => void;
onChange?: (value: ValueOf<F>) => void;
}

/**
Expand All @@ -49,11 +49,11 @@ export interface FieldRendererProps<Field extends FieldController<any>> {
*
* @template Field The rendered field.
*/
export function FieldRenderer<Field extends FieldController<any>>(props: FieldRendererProps<Field>): ReactElement {
export function FieldRenderer<F extends Field>(props: FieldRendererProps<F>): ReactElement {
const { field, eagerlyUpdated } = props;

const [, rerender] = useReducer(reduceCount, 0);
const handleChangeRef = useRef<FieldRendererProps<Field>['onChange']>();
const handleChangeRef = useRef<FieldRendererProps<F>['onChange']>();

handleChangeRef.current = props.onChange;

Expand Down
18 changes: 16 additions & 2 deletions packages/react/src/main/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { callOrGet, createField, Field, PluginInjector } from 'roqueform';
import { ValueAccessorContext } from './ValueAccessorContext';

// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types
type NoInfer<T> = T extends infer T ? T : never;
type NoInfer<T> = [T][T extends any ? 0 : never];

/**
* Creates the new field.
Expand All @@ -22,6 +22,20 @@ export function useField<Value = any>(): Field<Value | undefined>;
*/
export function useField<Value>(initialValue: Value | (() => Value)): Field<Value>;

/**
* Creates the new field enhanced by a plugin.
*
* @param initialValue The initial value assigned to the field.
* @param plugin The plugin injector that enhances the field.
* @returns The {@link Field} instance.
* @template Value The root field value.
* @template Plugin The plugin injected into the field.
*/
export function useField<Value, Plugin>(
initialValue: Value | (() => Value),
plugin: PluginInjector<Plugin>
): Field<Value, Plugin>;

/**
* Creates the new field enhanced by a plugin.
*
Expand All @@ -34,7 +48,7 @@ export function useField<Value>(initialValue: Value | (() => Value)): Field<Valu
export function useField<Value, Plugin>(
initialValue: Value | (() => Value),
plugin: PluginInjector<Plugin, NoInfer<Value>>
): Field<Plugin, Value>;
): Field<Value, Plugin>;

export function useField(initialValue?: unknown, plugin?: PluginInjector) {
const accessor = useContext(ValueAccessorContext);
Expand Down
Loading

0 comments on commit 00b1dfe

Please sign in to comment.