Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Improve the typing with generics
Browse files Browse the repository at this point in the history
  • Loading branch information
fabien0102 committed Aug 6, 2018
1 parent c22bc51 commit 5af9348
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 52 deletions.
35 changes: 19 additions & 16 deletions src/Get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps }
*/
export type ResolveFunction<T> = (data: any) => T;

export interface GetDataError<S> {
export interface GetDataError<TError> {
message: string;
data: S;
data: TError;
}

/**
* An enumeration of states that a fetchable
* view could possibly have.
*/
export interface States<S> {
export interface States<TData, TError> {
/** Is our view currently loading? */
loading: boolean;
/** Do we have an error in the view? */
error?: GetComponentState<S>["error"];
error?: GetComponentState<TData, TError>["error"];
}

/**
* An interface of actions that can be performed
* within Get
*/
export interface Actions<T> {
export interface Actions<TData> {
/** Refetches the same path */
refetch: () => Promise<T | null>;
refetch: () => Promise<TData | null>;
}

/**
Expand All @@ -46,7 +46,7 @@ export interface Meta {
/**
* Props for the <Get /> component.
*/
export interface GetComponentProps<T = {}, S = {}> {
export interface GetComponentProps<TData, TError> {
/**
* The path at which to request data,
* typically composed by parent Gets or the RestfulProvider.
Expand All @@ -59,14 +59,14 @@ export interface GetComponentProps<T = {}, S = {}> {
* @param data - data returned from the request.
* @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE.
*/
children: (data: T | null, states: States<S>, actions: Actions<T>, meta: Meta) => React.ReactNode;
children: (data: TData | null, states: States<TData, TError>, actions: Actions<TData>, meta: Meta) => React.ReactNode;
/** Options passed into the fetch call. */
requestOptions?: RestfulReactProviderProps["requestOptions"];
/**
* A function to resolve data return from the backend, most typically
* used when the backend response needs to be adapted in some way.
*/
resolve?: ResolveFunction<T>;
resolve?: ResolveFunction<TData>;
/**
* Should we wait until we have data before rendering?
* This is useful in cases where data is available too quickly
Expand All @@ -90,7 +90,7 @@ export interface GetComponentProps<T = {}, S = {}> {
* are implementation details and should be
* hidden from any consumers.
*/
export interface GetComponentState<T, S = {}> {
export interface GetComponentState<T, S> {
data: T | null;
response: Response | null;
error: GetDataError<S> | null;
Expand All @@ -102,15 +102,18 @@ export interface GetComponentState<T, S = {}> {
* is a named class because it is useful in
* debugging.
*/
class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<GetComponentState<T>>> {
public readonly state: Readonly<GetComponentState<T>> = {
class ContextlessGet<TData, TError> extends React.Component<
GetComponentProps<TData, TError>,
Readonly<GetComponentState<TData, TError>>
> {
public readonly state: Readonly<GetComponentState<TData, TError>> = {
data: null, // Means we don't _yet_ have data.
response: null,
loading: !this.props.lazy,
error: null,
};

public static defaultProps: Partial<GetComponentProps<{}>> = {
public static defaultProps = {
resolve: (unresolvedData: any) => unresolvedData,
};

Expand All @@ -120,7 +123,7 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
}
}

public componentDidUpdate(prevProps: GetComponentProps<T>) {
public componentDidUpdate(prevProps: GetComponentProps<TData, TError>) {
// If the path or base prop changes, refetch!
const { path, base } = this.props;
if (prevProps.path !== path || prevProps.base !== base) {
Expand Down Expand Up @@ -168,7 +171,7 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
const request = new Request(`${base}${requestPath || path || ""}`, this.getRequestOptions(thisRequestOptions));
const response = await fetch(request);

const data: T =
const data =
response.headers.get("content-type") === "application/json" ? await response.json() : await response.text();

if (!response.ok) {
Expand Down Expand Up @@ -205,7 +208,7 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
* in order to provide new `base` props that contain
* a segment of the path, creating composable URLs.
*/
function Get<T>(props: GetComponentProps<T>) {
function Get<TData = {}, TError = {}>(props: GetComponentProps<TData, TError>) {
return (
<RestfulReactConsumer>
{contextProps => (
Expand Down
39 changes: 26 additions & 13 deletions src/Mutate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { GetComponentState } from "./Get";
* An enumeration of states that a fetchable
* view could possibly have.
*/
export interface States<T = {}> {
export interface States<TData, TError> {
/** Is our view currently loading? */
loading: boolean;
/** Do we have an error in the view? */
error?: GetComponentState<T>["error"];
error?: GetComponentState<TData, TError>["error"];
}

/**
Expand Down Expand Up @@ -47,38 +47,48 @@ export interface MutateComponentCommonProps {
requestOptions?: RestfulReactProviderProps["requestOptions"];
}

export interface MutateComponentWithDelete extends MutateComponentCommonProps {
export interface MutateComponentWithDelete<TData, TError> extends MutateComponentCommonProps {
verb: "DELETE";
/**
* A function that recieves a mutation function, along with
* some metadata.
*
* @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE.
*/
children: (mutate: (resourceId?: string | {}) => Promise<Response>, states: States, meta: Meta) => React.ReactNode;
children: (
mutate: (resourceId?: string | {}) => Promise<Response>,
states: States<TData, TError>,
meta: Meta,
) => React.ReactNode;
}

export interface MutateComponentWithOtherVerb extends MutateComponentCommonProps {
export interface MutateComponentWithOtherVerb<TData, TError> extends MutateComponentCommonProps {
verb: "POST" | "PUT" | "PATCH";
/**
* A function that recieves a mutation function, along with
* some metadata.
*
* @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE.
*/
children: (mutate: (body?: string | {}) => Promise<Response>, states: States, meta: Meta) => React.ReactNode;
children: (
mutate: (body?: string | {}) => Promise<Response>,
states: States<TData, TError>,
meta: Meta,
) => React.ReactNode;
}

export type MutateComponentProps = MutateComponentWithDelete | MutateComponentWithOtherVerb;
export type MutateComponentProps<TData, TError> =
| MutateComponentWithDelete<TData, TError>
| MutateComponentWithOtherVerb<TData, TError>;

/**
* State for the <Mutate /> component. These
* are implementation details and should be
* hidden from any consumers.
*/
export interface MutateComponentState<S = {}> {
export interface MutateComponentState<TData, TError> {
response: Response | null;
error: GetComponentState<S>["error"];
error: GetComponentState<TData, TError>["error"];
loading: boolean;
}

Expand All @@ -87,8 +97,11 @@ export interface MutateComponentState<S = {}> {
* is a named class because it is useful in
* debugging.
*/
class ContextlessMutate extends React.Component<MutateComponentProps, MutateComponentState> {
public readonly state: Readonly<MutateComponentState> = {
class ContextlessMutate<TData, TError> extends React.Component<
MutateComponentProps<TData, TError>,
MutateComponentState<TData, TError>
> {
public readonly state: Readonly<MutateComponentState<TData, TError>> = {
response: null,
loading: false,
error: null,
Expand Down Expand Up @@ -146,12 +159,12 @@ class ContextlessMutate extends React.Component<MutateComponentProps, MutateComp
* in order to provide new `base` props that contain
* a segment of the path, creating composable URLs.
*/
function Mutate(props: MutateComponentProps) {
function Mutate<TError = {}, TData = {}>(props: MutateComponentProps<TData, TError>) {
return (
<RestfulReactConsumer>
{contextProps => (
<RestfulReactProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<ContextlessMutate {...contextProps} {...props} />
<ContextlessMutate<TData, TError> {...contextProps} {...props} />
</RestfulReactProvider>
)}
</RestfulReactConsumer>
Expand Down
49 changes: 26 additions & 23 deletions src/Poll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ interface Meta extends GetComponentMeta {
/**
* States of the current poll
*/
interface States<T> {
interface States<TData, TError> {
/**
* Is the component currently polling?
*/
polling: PollState<T>["polling"];
polling: PollState<TData, TError>["polling"];
/**
* Is the initial request loading?
*/
loading: PollState<T>["loading"];
loading: PollState<TData, TError>["loading"];
/**
* Has the poll concluded?
*/
finished: PollState<T>["finished"];
finished: PollState<TData, TError>["finished"];
/**
* Is there an error? What is it?
*/
error: PollState<T>["error"];
error: PollState<TData, TError>["error"];
}

/**
Expand All @@ -48,17 +48,17 @@ interface Actions {
/**
* Props that can control the Poll component.
*/
interface PollProps<T = {}> {
interface PollProps<TData, TError> {
/**
* What path are we polling on?
*/
path: GetComponentProps<T>["path"];
path: GetComponentProps<TData, TError>["path"];
/**
* A function that gets polled data, the current
* states, meta information, and various actions
* that can be executed at the poll-level.
*/
children: (data: T | null, states: States<T>, actions: Actions, meta: Meta) => React.ReactNode;
children: (data: TData | null, states: States<TData, TError>, actions: Actions, meta: Meta) => React.ReactNode;
/**
* How long do we wait between repeating a request?
* Value in milliseconds.
Expand All @@ -80,32 +80,32 @@ interface PollProps<T = {}> {
* @param data - The data returned from the poll.
* @param response - The full response object. This could be useful in order to stop polling when !response.ok, for example.
*/
until?: (data: T | null, response: Response | null) => boolean;
until?: (data: TData | null, response: Response | null) => boolean;
/**
* Are we going to wait to start the poll?
* Use this with { start, stop } actions.
*/
lazy?: GetComponentProps<T>["lazy"];
lazy?: GetComponentProps<TData, TError>["lazy"];
/**
* Should the data be transformed in any way?
*/
resolve?: GetComponentProps<T>["resolve"];
resolve?: GetComponentProps<TData, TError>["resolve"];
/**
* We can request foreign URLs with this prop.
*/
base?: GetComponentProps<T>["base"];
base?: GetComponentProps<TData, TError>["base"];
/**
* Any options to be passed to this request.
*/
requestOptions?: GetComponentProps<T>["requestOptions"];
requestOptions?: GetComponentProps<TData, TError>["requestOptions"];
}

/**
* The state of the Poll component. This should contain
* implementation details not necessarily exposed to
* consumers.
*/
interface PollState<T> {
interface PollState<TData, TError> {
/**
* Are we currently polling?
*/
Expand All @@ -121,15 +121,15 @@ interface PollState<T> {
/**
* What data are we holding in here?
*/
data: GetComponentState<T>["data"];
data: GetComponentState<TData, TError>["data"];
/**
* Are we loading?
*/
loading: GetComponentState<T>["loading"];
loading: GetComponentState<TData, TError>["loading"];
/**
* Do we currently have an error?
*/
error: GetComponentState<T>["error"];
error: GetComponentState<TData, TError>["error"];
/**
* Index of the last polled response.
*/
Expand All @@ -139,8 +139,11 @@ interface PollState<T> {
/**
* The <Poll /> component without context.
*/
class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollState<T>>> {
public readonly state: Readonly<PollState<T>> = {
class ContextlessPoll<TData, TError> extends React.Component<
PollProps<TData, TError>,
Readonly<PollState<TData, TError>>
> {
public readonly state: Readonly<PollState<TData, TError>> = {
data: null,
loading: !this.props.lazy,
lastResponse: null,
Expand All @@ -157,7 +160,7 @@ class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollStat

private keepPolling = !this.props.lazy;

private isModified = (response: Response, nextData: T) => {
private isModified = (response: Response, nextData: TData) => {
if (response.status === 304) {
return false;
}
Expand Down Expand Up @@ -208,7 +211,7 @@ class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollStat
response.headers.get("content-type") === "application/json" ? await response.json() : await response.text();

if (!this.isResponseOk(response)) {
const error = { message: `${response.status} ${response.statusText}`, data: response };
const error = { message: `${response.status} ${response.statusText}`, data: responseBody };
this.setState({ loading: false, lastResponse: response, data: responseBody, error });
throw new Error(`Failed to Poll: ${error}`);
}
Expand Down Expand Up @@ -265,7 +268,7 @@ class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollStat
absolutePath: `${base}${path}`,
};

const states: States<T> = {
const states: States<TData, TError> = {
polling,
loading,
error,
Expand All @@ -281,7 +284,7 @@ class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollStat
}
}

function Poll<T>(props: PollProps<T>) {
function Poll<TData = {}, TError = {}>(props: PollProps<TData, TError>) {
// Compose Contexts to allow for URL nesting
return (
<RestfulReactConsumer>
Expand Down

0 comments on commit 5af9348

Please sign in to comment.