-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Goals
This change improves the developer experience of defining view models so that it naturally supports expected application behavior:
- Atomically load all data projections: The
datafield remainsnulluntil all specifications have been resolved. Once available,datacontains fully-resolved, strongly typed, and non-null inner values. - Clearly represent loading state: The
loadingflag indicates whether data is still being fetched from a remote replicator. If all required data is already available in local storage,loadingisfalse, anddatais ready immediately. - Track background command activity: The
busyflag reflects whether any of the command functions are currently in progress, so developers can disable buttons or show spinners accordingly. - Surface errors in one place: The
errorfield combines both command errors and errors that occur while loading from the remote replicator. This simplifies error handling in the view. - Simplify view model declaration: Developers can use the
specification()helper function to declare each data requirement in a concise, strongly typed way without having to manage type annotations manually.
This helps application developers write views that are easy to reason about, avoid rendering partial data, and provide a seamless experience across online and offline modes.
Background
The useSpecification hook already provides a clear mechanism for loading data from a specification with typed results. We want to build on that with a new useViewModel hook. This combines specifications and commands to make the experience more ergonomic and type-safe for application developers.
Type Declarations
import { SpecificationOf } from 'jinaga';
type NullableElements<T extends readonly unknown[]> =
T extends [infer First, ...infer Rest]
? [First | null, ...NullableElements<Rest>]
: [];
export interface SpecificationInput<TGiven extends readonly unknown[], TProjection> {
specification: SpecificationOf<TGiven, TProjection>;
given: NullableElements<TGiven>;
}
export function specification<TGiven extends readonly unknown[], TProjection>(
spec: SpecificationOf<TGiven, TProjection>,
...given: NullableElements<TGiven>
): SpecificationInput<TGiven, TProjection> {
return { specification: spec, given };
}
type Command = () => Promise<void>;
type ViewModelInput = {
data?: Record<string, SpecificationInput<readonly unknown[], any>>;
commands?: Record<string, Command>;
};
type NonNullableDeep<T> = {
[K in keyof T]: T[K] extends object
? NonNullableDeep<NonNullable<T[K]>>
: NonNullable<T[K]>;
};
type ProjectionFromSpecInput<I> =
I extends SpecificationInput<readonly unknown[], infer TProjection>
? NonNullableDeep<TProjection>[]
: never;
type DataOutput<T extends Record<string, SpecificationInput<readonly unknown[], any>>> = {
[K in keyof T]: ProjectionFromSpecInput<T[K]>;
};
type ViewModelOutput<I extends ViewModelInput> = {
data: I['data'] extends infer D
? D extends Record<string, SpecificationInput<readonly unknown[], any>>
? DataOutput<D> | null
: null
: null;
commands: I['commands'] extends Record<string, Command> ? I['commands'] : {};
error: Error | null;
loading: boolean;
busy: boolean;
};
declare function useViewModel<I extends ViewModelInput>(
input: I
): ViewModelOutput<I>;Example Usage
const viewModel = useViewModel({
data: {
projects: specification(projectsOfUser, currentUser),
tasks: specification(tasksOfProject, selectedProject)
},
commands: {
assignTask: async () => { /* ... */ }
}
});
// viewModel.data?.projects[0].title // ✅ Fully typed, non-null
// viewModel.data?.tasks[0].dueDate // ✅ Typed deeply, never undefined/nullAcceptance Criteria
- The
useViewModelhook returns a fully inferred and strongly typeddataobject. - The
datafield isnullwhile loading or in error, but otherwise contains only fully-resolved values (noundefinedornullsubfields). - The
specification()helper provides a clean way to constructSpecificationInputobjects with proper inference of bothTGivenandTProjection. - No use of
anyin the final types exposed to consumers. - The API remains backward-compatible with existing use cases or a migration guide is included.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels