Skip to content

Commit

Permalink
feat: Link + helper files to Typescript (#1023)
Browse files Browse the repository at this point in the history
* feat: port createMatchEnhancer

* feat: port Link

* feat: resolveRenderArgs to TS

* feat: getRenderArgs, ResolverUtils to TS

* rm comments

* rm activeClassname default arg

* rm Link type guard
  • Loading branch information
golota60 committed Jan 1, 2023
1 parent 92425ca commit 556b3e5
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 71 deletions.
41 changes: 30 additions & 11 deletions src/Link.js → src/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import useEventCallback from '@restart/hooks/useEventCallback';
import React from 'react';
import warning from 'tiny-warning';

import { LinkInjectedProps, LinkProps } from './typeUtils';
import useRouter from './useRouter';

// TODO: Try to type this & simplify those types in next breaking change.
function Link({
as: Component = 'a',
to,
Expand All @@ -15,8 +17,9 @@ function Link({
exact = false,
onClick,
target,
children,
...props
}) {
}: any) {
const { router, match } = useRouter() || {
match: propsMatch,
router: propsRouter,
Expand Down Expand Up @@ -52,41 +55,43 @@ function Link({
});

if (__DEV__ && typeof Component !== 'function') {
for (const wrongPropName of ['component', 'Component']) {
const wrongPropValue = props[wrongPropName];
for (const wrongPropName of ['component', 'Component'] as const) {
const wrongPropValue = (props as any)[wrongPropName];
if (!wrongPropValue) {
continue;
}

warning(
false,
'Link to %s with `%s` prop `%s` has an element type that is not a component. The expected prop for the link component is `as`.',
JSON.stringify(to),
wrongPropName,
wrongPropValue.displayName || wrongPropValue.name || 'UNKNOWN',
`Link to ${JSON.stringify(to)} with \`${wrongPropName}\` prop \`${
wrongPropValue.displayName || wrongPropValue.name || 'UNKNOWN'
}\` has an element type that is not a component. The expected prop for the link component is \`as\`.`,
);
}
}

const href = router.createHref(to);
const childrenIsFunction = typeof props.children === 'function';
const childrenIsFunction = typeof children === 'function';

if (childrenIsFunction || activeClassName || activeStyle || activePropName) {
const toLocation = router.createLocation(to);
const active = router.isActive(match, toLocation, { exact });

if (childrenIsFunction) {
return props.children({ href, active, onClick: handleClick });
const add = { href, active, onClick: handleClick };
return children(add);
}

if (active) {
if (activeClassName) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props.className = props.className
? `${props.className} ${activeClassName}`
? `${props} ${activeClassName}`
: activeClassName;
}

if (activeStyle) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props.style = { ...props.style, ...activeStyle };
}
}
Expand All @@ -105,4 +110,18 @@ function Link({
);
}

export default Link;
// eslint-disable-next-line react/prefer-stateless-function
declare class LinkType<
TInner extends React.ElementType = never,
TInnerWithActivePropName extends React.ComponentType<
LinkInjectedProps & { [activePropName in TActivePropName]: boolean }
> = never,
TActivePropName extends string = never,
> extends React.Component<
LinkProps<TInner, TInnerWithActivePropName, TActivePropName>
> {
// eslint-disable-next-line react/static-property-placement, react/no-unused-class-component-methods
props: LinkProps<TInner, TInnerWithActivePropName, TActivePropName>;
}

export default Link as unknown as LinkType;
1 change: 0 additions & 1 deletion src/Redirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ if (__DEV__) {

// This actually doesn't extend a React.Component, but we need consumer to think that it does
declare class RedirectType extends React.Component<RedirectOptions> {
// @ts-ignore
constructor(config: RedirectOptions);
}

Expand Down
53 changes: 33 additions & 20 deletions src/ResolverUtils.js → src/ResolverUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import isPromise from 'is-promise';
import { setImmediate } from 'tiny-set-immediate';
import warning from 'tiny-warning';

import { Match, RouteIndices, RouteMatch, RouteObjectBase } from './typeUtils';

const UNRESOLVED = {};

/**
Expand All @@ -10,28 +12,29 @@ const UNRESOLVED = {};
*
* If the value is not a promise it's simply returned
*/
export function checkResolved(value) {
export function checkResolved<T extends Promise<T>>(value: T): Promise<T> | T {
if (!isPromise(value)) {
return value;
}

return Promise.race([
const ret = Promise.race<T>([
value,
new Promise((resolve) => {
setImmediate(resolve, UNRESOLVED);
}),
]);

return ret;
}

export function isResolved(value) {
export function isResolved<T>(value: T | Record<string, unknown>): value is T {
return value !== UNRESOLVED;
}

function accumulateRouteValuesImpl(
routeValues,
routeIndices,
callback,
initialValue,
routeValues: RouteMatch[],
routeIndices: RouteIndices,
callback: (...args: any[]) => Record<string, unknown>,
initialValue: Record<string, unknown>,
) {
const accumulated = [];
let value = initialValue;
Expand Down Expand Up @@ -59,10 +62,11 @@ function accumulateRouteValuesImpl(
}

export function accumulateRouteValues(
routeValues,
routeIndices,
callback,
initialValue,
routeValues: RouteMatch[],
routeIndices: RouteIndices,
// TODO: type this better
callback: (...args: any[]) => Record<string, unknown>,
initialValue: Record<string, unknown>,
) {
return accumulateRouteValuesImpl(
[...routeValues],
Expand All @@ -72,44 +76,53 @@ export function accumulateRouteValues(
);
}

export function getRouteMatches(match) {
export function getRouteMatches(match: Match) {
return match.routes.map((route, i) => ({
...match,
route,
routeParams: match.routeParams[i],
}));
}

export function getRouteValue(match, getGetter, getValue) {
export function getRouteValue(
match: RouteMatch,
getGetter: (route: RouteObjectBase) => RouteObjectBase['getData'],
getValue: (route: RouteObjectBase) => RouteObjectBase['data'],
) {
const { route } = match;
const getter = getGetter(route);
return getter ? getter.call(route, match) : getValue(route);
}

// This is a little more versatile than if we only passed in keys.
export function getRouteValues(routeMatches, getGetter, getValue) {
export function getRouteValues(
routeMatches: RouteMatch[],
getGetter: (route: RouteObjectBase) => RouteObjectBase['getData'],
getValue: (route: RouteObjectBase) => RouteObjectBase['data'],
) {
return routeMatches.map((match) =>
getRouteValue(match, getGetter, getValue),
);
}

function getRouteGetComponent(route) {
function getRouteGetComponent(route: RouteObjectBase) {
return route.getComponent;
}

function getRouteComponent(route) {
function getRouteComponent(route: RouteObjectBase) {
if (__DEV__ && route.component) {
warning(
route.Component,
'Route with `component` property `%s` has no `Component` property. The expected property for the route component is `Component`.',
route.component.displayName || route.component.name,
`Route with \`component\` property \`${
route.component.displayName || route.component.name
}\` has no \`Component\` property. The expected property for the route component is \`Component\`.`,
);
}

return route.Component;
}

// This should be common to most resolvers, so make it available here.
export function getComponents(routeMatches) {
export function getComponents(routeMatches: RouteMatch[]) {
return getRouteValues(routeMatches, getRouteGetComponent, getRouteComponent);
}
21 changes: 13 additions & 8 deletions src/createMatchEnhancer.js → src/createMatchEnhancer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import FarceActionTypes from 'farce/ActionTypes';
import { applyMiddleware } from 'redux';
import { Middleware, Store, StoreEnhancer, applyMiddleware } from 'redux';

import ActionTypes from './ActionTypes';
import Matcher from './Matcher';
import { FoundState, FoundStoreExtension, RouteConfig } from './typeUtils';

function createMatchMiddleware(matcher, getFound) {
return function matchMiddleware(store) {
function createMatchMiddleware(
matcher: Matcher,
getFound: ({ found }: any) => FoundState,
): Middleware {
return function matchMiddleware(store: Store) {
return (next) => (action) => {
const { type, payload } = action;
if (type !== FarceActionTypes.UPDATE_LOCATION) {
Expand Down Expand Up @@ -35,9 +40,9 @@ function createMatchMiddleware(matcher, getFound) {
}

export default function createMatchEnhancer(
matcher,
getFound = ({ found }) => found,
) {
matcher: Matcher,
getFound = ({ found }: any) => found,
): StoreEnhancer<{ found: FoundStoreExtension }> {
return function matchEnhancer(createStore) {
return (...args) => {
const middlewareEnhancer = applyMiddleware(
Expand All @@ -46,10 +51,10 @@ export default function createMatchEnhancer(

const store = middlewareEnhancer(createStore)(...args);

function replaceRouteConfig(routeConfig) {
function replaceRouteConfig(routeConfig: RouteConfig) {
matcher.replaceRouteConfig(routeConfig);

store.dispatch({
store.dispatch<any>({
type: FarceActionTypes.UPDATE_LOCATION,
payload: getFound(store.getState()).match.location,
});
Expand Down
11 changes: 0 additions & 11 deletions src/getRenderArgs.js

This file was deleted.

15 changes: 15 additions & 0 deletions src/getRenderArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import resolveRenderArgs, { ResolveRender } from './resolveRenderArgs';
import { Router } from './typeUtils';

export default async function getRenderArgs(
router: Router,
props: any,
): Promise<ResolveRender> {
let elements;

for await (elements of resolveRenderArgs(router, props)) {
// Nothing to do here. We just need the last value from the iterable.
}

return elements as ResolveRender;
}
49 changes: 37 additions & 12 deletions src/resolveRenderArgs.js → src/resolveRenderArgs.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import HttpError from './HttpError';
import {
Match,
RenderArgsElements,
ResolvedElement,
Resolver,
RouteIndices,
RouteObject,
Router,
} from './typeUtils';

function foldElements(elementsRaw, routeIndices) {
function foldElements(
elementsRaw: Array<ResolvedElement>,
routeIndices: RouteIndices,
): RenderArgsElements {
const elements = [];

for (const routeIndex of routeIndices) {
if (typeof routeIndex === 'object') {
// Reshape the next elements in the elements array to match the nested
// tree structure corresponding to the route groups.
const groupElements = {};
const groupElements: any = {};
Object.entries(routeIndex).forEach(([groupName, groupRouteIndices]) => {
groupElements[groupName] = foldElements(
elementsRaw,
groupRouteIndices,
);
const folded = foldElements(elementsRaw, groupRouteIndices);
groupElements[groupName] = folded;
});

elements.push(groupElements);
Expand All @@ -26,13 +36,28 @@ function foldElements(elementsRaw, routeIndices) {
return elements;
}

interface AugmentedMatchType extends Match {
routes: RouteObject[];
router: Router;
context: any;
}

export interface ResolveRender extends AugmentedMatchType {
error?: any;
elements?: RenderArgsElements;
}

export default async function* resolveRenderArgs(
router,
{ match, matchContext, resolver },
) {
const routes = router.matcher.getRoutes(match);
router: Router,
{
match,
matchContext,
resolver,
}: { match: Match; matchContext: any; resolver: Resolver },
): AsyncGenerator<ResolveRender, undefined> {
const routes = router.matcher.getRoutes(match)!;

const augmentedMatch = {
const augmentedMatch: AugmentedMatchType = {
...match,
routes,
router, // Convenience access for route components.
Expand All @@ -52,7 +77,7 @@ export default async function* resolveRenderArgs(
elements: elements && foldElements([...elements], match.routeIndices),
};
}
} catch (e) {
} catch (e: any) {
if (e.isFoundHttpError) {
yield { ...augmentedMatch, error: e };
return;
Expand Down

0 comments on commit 556b3e5

Please sign in to comment.