Skip to content

Instantly share code, notes, and snippets.

@webbower
Last active July 9, 2022 00:45
Show Gist options
  • Save webbower/6370ae71b58e18dae5fc187e51534f59 to your computer and use it in GitHub Desktop.
Save webbower/6370ae71b58e18dae5fc187e51534f59 to your computer and use it in GitHub Desktop.
Webbower Standard Library
import { describe } from 'src/utils/testing';
import { renderHook } from '@testing-library/react-hooks';
//////// useConstant.test.ts ////////
import useConstant from 'src/hooks/useConstant';
describe('useConstant()', async assert => {
const testStateValue = { value: 'abc' };
const { result } = renderHook(() => useConstant(testStateValue));
assert({
given: 'a value',
should: 'return the same value',
actual: result.current === testStateValue,
expected: true,
});
});
//////// useObjectState.test.ts ////////
import { renderHook, act } from '@testing-library/react-hooks';
import { describe } from 'src/utils/testing';
import type { RenderResult } from '@testing-library/react-hooks';
import { useObjectState } from './useObjectState';
import type { ObjectStateApi } from './useObjectState';
describe('useObjectState()', async assert => {
type TestObjectState = {
str: string;
num: number;
list: string[];
flag: boolean;
};
type HookReturnType = [TestObjectState, ObjectStateApi<TestObjectState>];
const getHookStateValue = (result: RenderResult<HookReturnType>) => result.current[0];
const getHookStateApi = (result: RenderResult<HookReturnType>) => result.current[1];
{
let messWithInitialState = false;
const shouldUseMessedWithInitialState = () => messWithInitialState;
const initialState = {
str: '',
num: 0,
list: ['one', 'two', 'three'],
flag: false,
};
const { result } = renderHook(() =>
useObjectState<TestObjectState>(
shouldUseMessedWithInitialState()
? {
...initialState,
num: 1,
}
: initialState
)
);
const initialApi = getHookStateApi(result);
assert({
given: 'hook initialization',
should: 'return the initial state value and the API',
actual: [getHookStateValue(result), getHookStateApi(result)],
expected: [initialState, initialApi],
});
act(() => {
getHookStateApi(result).setField('num', 1);
});
assert({
given: 'updating one field with the `.setField()` API function',
should: 'return the updated state and the same API object by identity',
actual: [getHookStateValue(result), getHookStateApi(result) === initialApi],
expected: [
{
str: '',
num: 1,
list: ['one', 'two', 'three'],
flag: false,
},
true,
],
});
act(() => {
getHookStateApi(result).update({ str: 'updated', flag: true, list: [] });
// Simulate an unstable initialState arg to ensure that `.reset()` will correctly set the original
// initialState when called. This unstable arg may happen when the initialState is set by a prop instead of
// a hard-coded value.
messWithInitialState = true;
});
assert({
given: 'updating object state with the `.update()` API function',
should: 'return the updated state',
actual: getHookStateValue(result),
expected: {
str: 'updated',
num: 1,
list: [],
flag: true,
},
});
act(() => {
getHookStateApi(result).reset();
});
assert({
given: 'updating object state with the `.reset()` API function',
should: 'return the original initial state',
actual: getHookStateValue(result),
expected: initialState,
});
act(() => {
getHookStateApi(result).set({
str: 'set',
num: -1,
list: ['1', '2', '3'],
flag: true,
});
});
assert({
given: 'updating the whole object state with the `.set()` API function',
should: 'return the updated state',
actual: getHookStateValue(result),
expected: {
str: 'set',
num: -1,
list: ['1', '2', '3'],
flag: true,
},
});
}
});
//////// useMilestones.test.ts ////////
import { renderHook, act } from '@testing-library/react-hooks';
import { describe, sinon } from 'src/utils/testing';
import setProp from 'src/utils/setProp';
import type { RenderResult } from '@testing-library/react-hooks';
import type { PlainObject } from './types';
import { useMilestones } from './useMilestones';
describe('useMilestones()', async assert => {
type HookReturnType = ReturnType<typeof useMilestones>;
const getHookStateValue = (result: RenderResult<HookReturnType>) => result.current[1];
const getHookStateApi = (result: RenderResult<HookReturnType>) => result.current[0];
// Helper function to assert shape of hook API object
const transformObjectKeysToTypes = (obj: PlainObject): PlainObject =>
Object.entries(obj).reduce((res, [key, value]) => setProp(res, key, typeof value), {});
{
const onCompletionSpy = sinon.spy();
const { result } = renderHook(() =>
useMilestones({ trueBool: true, falseBool: false, positive: 2, negative: -2 }, onCompletionSpy)
);
const api = getHookStateApi(result);
assert({
given: 'the hook in its initial state',
should: 'have its completion state as false',
actual: getHookStateValue(result),
expected: false,
});
assert({
given: 'the hook in its initial state',
should: 'have object keys in the returned API that match the keys of the initialization object',
actual: transformObjectKeysToTypes(api),
expected: { trueBool: 'function', falseBool: 'function', positive: 'function', negative: 'function' },
});
act(() => {
api.trueBool();
});
assert({
given: 'the first milestone function is called',
should: 'still have its completion state as false and the completion callback was not called',
actual: [getHookStateValue(result), onCompletionSpy.called],
expected: [false, false],
});
act(() => {
api.falseBool();
api.positive();
api.positive();
api.negative();
api.negative();
});
assert({
given: 'the rest of the milestone functions are called to completion',
should: 'have its completion state as true and the completion callback was called',
actual: [getHookStateValue(result), onCompletionSpy.called],
expected: [true, true],
});
act(() => {
api.trueBool();
api.falseBool();
api.positive();
api.negative();
});
assert({
given: 'calling the milestone functions again after all milestones are completed',
should: 'keep the milestone completed state as true and not call the completion callback again',
actual: [getHookStateValue(result), onCompletionSpy.callCount],
expected: [true, 1],
});
}
{
const { result, rerender } = renderHook(() => useMilestones({ bool: true, number: 2 }));
const firstRenderApi = getHookStateApi(result);
act(() => {
rerender();
});
const secondRenderApi = getHookStateApi(result);
assert({
given: 'multiple renders',
should: 'have returned hook API be referentially stable',
actual: [
firstRenderApi === secondRenderApi,
firstRenderApi.bool === secondRenderApi.bool,
firstRenderApi.number === secondRenderApi.number,
],
expected: [true, true, true],
});
}
});
//////// useConsumedUrlParams.test.ts ////////
import { sinon } from 'sinon';
import { renderHook } from '@testing-library/react-hooks';
import proxyquire from 'proxyquire';
import type { Location, History } from 'history';
import { useConsumedUrlParams as actualUseConsumedUrlParams } from 'src/hooks';
export const locationFixtureFactory = (location: Partial<Location> = {}): Location => ({
key: '',
pathname: '/',
search: '',
state: null,
hash: '',
...location,
});
export const historyFixtureFactory = (history: Partial<History> = {}): History => ({
length: 1,
action: 'PUSH',
location: locationFixtureFactory(),
push: noop,
replace: noop,
go: noop,
goBack: noop,
goForward: noop,
block: () => noop,
listen: () => noop,
createHref: () => 'http://localhost.com',
...history,
});
const mockReplace = sinon.spy();
const { useConsumedUrlParams } = proxyquire<{ useConsumedUrlParams: typeof actualUseConsumedUrlParams }>(
'./useConsumedUrlParams',
{
'react-router': {
useHistory: () => ({
replace: mockReplace,
}),
},
}
);
describe('useConsumedUrlParams()', async assert => {
{
const testLocation = locationFixtureFactory({
search: 'foo=1&bar=%2Ffoo%2Fbar.jpg&baz=http%3A%2F%2Fwww.example.com%2Ffoo%2Fbar.jpg%3Fquux%3Dhello%2520world',
});
const { result } = renderHook(() => useConsumedUrlParams(testLocation));
assert({
given: 'no params list',
should: 'returns an empty object',
actual: result.current,
expected: {},
});
assert({
given: 'no params list',
should: 'does not replace history',
actual: mockReplace.notCalled,
expected: true,
});
}
{
mockReplace.resetHistory();
const testLocation = locationFixtureFactory({
pathname: '/test/pathname',
search: 'foo=1&bar=%2Ffoo%2Fbar.jpg&baz=http%3A%2F%2Fwww.example.com%2Ffoo%2Fbar.jpg%3Fquux%3Dhello%2520world',
});
const { result } = renderHook(() => useConsumedUrlParams(testLocation, ['foo', 'bar']));
assert({
given: 'params list',
should: 'returns the consumed params',
actual: result.current,
expected: {
foo: '1',
bar: '/foo/bar.jpg',
},
});
assert({
given: 'params list',
should: 'calls history replace exactly once',
actual: mockReplace.callCount,
expected: 1,
});
assert({
given: 'params list',
should: 'calls history replace reduced params',
actual: mockReplace.args[0],
expected: [
{
pathname: testLocation.pathname,
search: '?baz=http%3A%2F%2Fwww.example.com%2Ffoo%2Fbar.jpg%3Fquux%3Dhello%2520world',
},
],
});
}
});
import { useRef, useEffect, useState } from 'react';
const noop = () => {};
//////// useConstant.ts ////////
// Old one
const useConstant = <StateType>(val: StateType): StateType => useRef(val).current;
// Experimental opt-in Lazy eval
const useConstant = <StateType>(
val: StateType,
lazy = false
): StateType => {
const ref = useRef();
if (!ref.current) {
ref.current = lazy && typeof val === 'function' ? val() : val;
}
return ref.current;
};
//////// useEffectOnMounted.ts ////////
const useEffectOnMounted = cb => {
useEffect(cb, []);
};
//////// useBooleanState.ts ////////
interface BooleanStateApi {
on: () => void;
off: () => void;
toggle: () => void;
}
const useBooleanState = (initialState = false): [boolean, BooleanStateApi] => {
const [state, setState] = useState(initialState);
const api = useConstant<BooleanStateApi>({
on: () => setState(true),
off: () => setState(false),
toggle: () => setState(current => !current),
});
return [state, api];
};
//////// useObjectState.ts ////////
import type { Dispatch, SetStateAction } from 'react';
import type { PlainObject, Values } from './types';
export type ObjectStateApi<T extends PlainObject = PlainObject> = {
set: Dispatch<SetStateAction<T>>;
reset: () => void;
setField: (fieldName: keyof T, fieldValue: Values<T>) => void;
update: (newPartialState: Partial<T>) => void;
};
/**
* Custom hook that wraps @see useState and provides better state updater primitive functions for dealing with POJO
* state.
*/
const useObjectState = <T extends PlainObject = PlainObject>(
initialState: T = {} as T
): [T, ObjectStateApi<T>] => {
const [state, setState] = useState(initialState);
const api = useConstant<ObjectStateApi<T>>({
/**
* Overwrite the whole state. This is the default state setter.
*/
set: setState,
/**
* Reset the state to the first `initialState`
*/
reset: () => {
// NOTE This will be the first initialState because `useConstant()` for the API captures the first value in
// this function definition closure.
setState(initialState);
},
/**
* Set a single field to a specific value
*/
setField: (fieldName, fieldValue) => {
setState(current => ({
...current,
[fieldName]: fieldValue,
}));
},
/**
* Update only the fields provided in the argument
*/
update: newPartialState => {
setState(current => ({
...current,
...newPartialState,
}));
},
});
return [state, api];
};
//////// useFieldState.ts ////////
export const NO_ERROR_VALUE = null;
export type FieldState<E = string> = {
value: string;
error: E | typeof NO_ERROR_VALUE;
touched: boolean;
};
type FieldStateConstructor = {
(state?: Partial<FieldState>): FieldState;
};
export const FieldState: FieldStateConstructor = (
{ value = '', error = NO_ERROR_VALUE, touched = false } = {} as FieldState
) => ({
value,
error,
touched,
});
type FieldStateApi = ObjectStateApi<FieldState> & {
setFieldValue: (newValue: FieldState['value']) => void;
clearFieldValue: () => void;
setFieldError: (newError: FieldState['error']) => void;
clearFieldError: () => void;
fieldWasTouched: () => void;
};
export const useFieldState = (initialState: FieldState = FieldState()): [FieldState, FieldStateApi] => {
const [state, objectApi] = useObjectState(initialState);
const api = useConstant<FieldStateApi>({
...objectApi,
setFieldValue(newValue) {
objectApi.setField('value', newValue);
},
clearFieldValue() {
objectApi.setField('value', '');
},
setFieldError(newError) {
objectApi.setField('error', newError);
},
clearFieldError() {
objectApi.setField('error', NO_ERROR_VALUE);
},
fieldWasTouched() {
objectApi.setField('touched', true);
},
});
return [state, api];
};
export const getFieldValue = ({ value }: FieldState): FieldState['value'] => value;
export const doesFieldHaveValue = (fieldState: FieldState): boolean => getFieldValue(fieldState) !== '';
export const getFieldFirstError = ({ error }: FieldState): FieldState['error'] => error;
export const isFieldValid = (fieldState: FieldState): boolean => !!getFieldFirstError(fieldState);
export const isFieldTouched = ({ touched }: FieldState): FieldState['touched'] => touched;
//////// useComponentApi.ts ////////
import isEmptyObject from 'src/utils/isEmptyObject';
type ApiFunction = (...args: any[]) => unknown;
export type ComponentApi = Record<string, ApiFunction>;
export type OpaqueApiSetter = {
set(api: ComponentApi, id?: string): void;
unset(id?: string): void;
};
/**
* The type for a `useComponentApi()` that only holds one, non-namespaced component API
*/
type SingleComponentApi = ComponentApi;
/**
* The type for a `useComponentApi()` that holds multiple namespaced component APIs
*/
type MultiComponentApi = Record<string, SingleComponentApi>;
/**
* Define a public API for a component that can be passed up to parent components
*
* These hooks provide the ability for a component to expose a public API for performing programmatic actions defined by
* the component. `useComponentApiDef()` is used to defined the component API and connect it to the paired
* `useComponentApi()` which holds the API for one or more child components by the parent component. It borrows some
* concepts from native DOM refs. Where a ref usually provides a variable to hold a native DOM node, requiring the
* component holding it to manually operate on the DOM node, and API is a defined set of functionality for the component
* which allows a component to define a set of actions for a parent to use which can provide consistency as public APIs
* are meant to. For example, with a custom component:
*
* - Using a ref for the <input> element of the custom component, if you want to programmatically focus on the <input>,
* you would call the `.focus()` method on the DOM node. If there was any other behavior the component needed to
* happen when focusing on the element, each parent component would need to include that manually.
* - Using an API for the custom component, the exposed `.focus()` function would not only focus on the <input> element,
* the custom component can define additional things that need to happen for consistency when programmatically
* focusing.
*
* These custom hooks store stable copies of the defined and held component APIs.
*
* - `useComponentApiDef()` takes one required arg (the defined API object) and 2 optional args: the `setApi` that comes
* from `useComponentApi()` and an id to optionally namespace the API for the holding component in case it needs to
* hold multiple component APIs. It returns the API defined by the first arg for use inside the defining component.
* - `useComponentApi()` takes zero args and returns a tuple of the defined API(s) and the opaque `setApi` object which
* is passed to the child components that define an API that will be used. If the child components include namespaces,
* the API object have multiple APIs under namespaces (e.g. a form holding APIs for its form fields would have each
* component API namespaced under the `name` prop value of the form field component). If the child component does not
* define a namespace, then its API will be the top-level value of the API. These are referred to as MultiComponentApi
* and SingleComponentApi, respectively.
*
* SingleComponentApi example:
* <code>
* interface ChildApi extends ComponentApi {
* func1: (...) => void;
* func2: (...) => void;
* }
*
* const Child = ({ api: setApi, onEvent, ...props }) => {
* useComponentApiDef<ChildApi>({
* func1: (...) => {...},
* func1: (...) => {...},
* }, setApi);
*
* onEvent(...);
*
* return (...);
* };
*
* const Parent => (props) => {
* const [childApi, setApi] = useComponentApi<ChildApi>();
*
* return (
* <Child api={setApi} onEvent={() => { childApi.func1(); }} />
* );
* };
* </code>
*
* MultiComponentApi example:
* <code>
* interface InputFieldAPi extends ComponentApi {
* func1: (...) => void;
* func2: (...) => void;
* }
*
* const InputField = ({ name, api: setApi, onChange, ...props }) => {
* useComponentApiDef<ChildApi>({
* func1: (...) => {...},
* func1: (...) => {...},
* }, setApi, name);
*
* onEvent(...);
*
* return (...);
* };
*
* const Form => (props) => {
* // The type for MultiComponentApi must resolved to an object of string keys and `ComponentApi` values. If all the
* // field component APIs are the same type, you can use Record<"fieldName1" | "fieldName2" | "fieldNameN", FieldApi>
* // but if you have different types of fields, you will need to explicitly define the object type for each key.
* const [fieldApis, setApi] = useComponentApi<Record<'firstName' | 'lastName', InputFieldApi>>();
*
* return (
* <InputField name="firstName" api={setApi} onEvent={() => { fieldApis.firstName.func1(); }} />
* <InputField name="lastName" api={setApi} onEvent={() => { fieldApis.lastName.func1(); }} />
* );
* };
* </code>
*/
export const useComponentApiDef = <Api extends ComponentApi>(
apiObject: Api,
setApi?: OpaqueApiSetter,
id?: string
): Api => {
const apiRef = useRef<Api>({} as Api);
useEffectOnMounted(() => {
apiRef.current = apiObject;
// Only attempt to set the API if the setter was provided
if (setApi) {
setApi.set(apiRef.current, id);
}
return () => {
// Only attempt to unset the API if the setter was provided
if (setApi) {
setApi.unset(id);
}
};
});
return apiRef.current;
};
export const useComponentApi = <Api extends SingleComponentApi | MultiComponentApi>(
forceSingleComponentApi = false
): [Api, OpaqueApiSetter] => {
// Store the original setting to lock in single component API so that it can't be accidentally overridden later and
// cause unexpected behavior
const shouldBeSingleComponentApi = useConstant(forceSingleComponentApi);
const api = useRef<Api>({} as Api);
const setApi = useConstant<OpaqueApiSetter>({
set: (componentApi, id) => {
if (!shouldBeSingleComponentApi && id) {
(api.current as MultiComponentApi)[id] = componentApi;
} else {
// If there are some components with that provide ids and one or more that don't, we want to avoid
// clobbering all of the ones with namespaces by a component API without an ID
if (!isEmptyObject(api.current)) {
throw new Error(
'Unable to set single API. API object appears to have namespaced APIs already attached and setting single API would override all of them.'
);
}
if (Object.keys(api.current).length > 0) {
throw new Error(
'Attempting to override single component API that is already set for this component.'
);
}
(api.current as SingleComponentApi) = componentApi;
}
},
unset: id => {
if (!shouldBeSingleComponentApi && id) {
// If we have an `id` and ONLY if that `id` exists as a namespace on the `api` object do we delete it
if ((api.current as MultiComponentApi)[id]) {
delete (api.current as MultiComponentApi)[id];
}
} else {
(api.current as SingleComponentApi) = {};
}
},
});
return [api.current, setApi];
};
//////// useMilestones.ts ////////
import deepEqual from 'fast-deep-equal';
type MilestoneGoal = boolean | number;
type MilestonesData = Record<string, MilestoneGoal>;
type MilestoneTrigger = () => void;
type MilestonesTriggers = Record<string, MilestoneTrigger>;
const determineInitialMilestoneState = (goal: MilestoneGoal) => (typeof goal === 'boolean' ? !goal : 0);
/**
* The useMilestones custom hook allows you to track state for the completion of a single state that is based on
* multiple goals (e.g. one user interaction must happen once and another interaction must happen n times).
*
* The hook takes a dict of milestone names and target boolean or number goals and an optional callback function that
* will be called when all milestones are completed. It returns a dict with keys that match the milestones dict that map
* to functions which will progress that milestone (boolean goals are flipped, number goals are incremented/decremented
* depending on whether they are positive or negative goals values) and a boolean signaling the completion of the
* milestones.
*
* <code>
* // Contrived example to determine whether a form field has been "touched" (it has received focus and had more than 5
* // changes to its value)
* const [
* { blurred: fieldWasBlurred, changedCount: fieldValueWasChanged },
* isTouched,
* ] = useMilestones({ touched: true, changed: 5 }, () => { console.log('Field was touched'); });
*
* <input
* onChange={ev => {
* const { name, value } = ev.target;
* // ... other state changes
* fieldValueWasChanged();
* }}
* onBlur={ev => {
* fieldWasBlurred();
* }}
* />
*
* {isTouched ? <p>'Field was touched'</p> : null}
* </code>
*
* @param milestones A dict of milestone names and target completion values (goals)
* @param onMilestonesCompleted An optional callback function that will be called when all the milestones are completed
* @returns A tuple where the first index is the milestones progress triggers and the second index is a boolean that
* will be true when all the milestone goals are reached and false prior to that.
*/
export const useMilestones = (
milestones: MilestonesData,
onMilestonesCompleted: () => void = noop
): [MilestonesTriggers, boolean] => {
// Cache the original milestones and completion callback
const finalGoal = useConstant(milestones);
const completionCallback = useConstant(onMilestonesCompleted);
// We need to iterate on the milestones twice so save calling Object.entries() twice
const milestonesEntries = Object.entries(finalGoal);
// Generate the internal milestone tracking state. Set with function signature for initial state to prevent
// recalculation on re-renders:
// - Booleans are initialized as their inverse
// - Numbers are initialized at 0
const [milestonesState, setMilestoneState] = useState(() =>
milestonesEntries.reduce<MilestonesData>(
(state, [name, goal]) => (state[name] = determineInitialMilestoneState(goal), state),
{}
)
);
// Produce the returned hook API to trigger the completion/progress of milestone goals. Use function style useState
// initialState to prevent recalculation on re-renders.
const [triggers] = useState(() =>
milestonesEntries.reduce<MilestonesTriggers>(
(ts, [name, goal]) => (
ts[name] = () => {
setMilestoneState(current => {
const currentMilestone = current[name];
// If the current value for the milestone doesn't match the goal, perform the update logic
if (currentMilestone !== goal) {
let nextValue: MilestoneGoal | null = null;
// Boolean goals get flipped
if (typeof currentMilestone === 'boolean') {
nextValue = !currentMilestone;
}
// Number goals get incremented or decremented
if (typeof currentMilestone === 'number') {
nextValue = goal < 0 ? currentMilestone - 1 : currentMilestone + 1;
}
// If a goal value was updated, update the internal state
if (nextValue != null) {
return {
...current,
[name]: nextValue,
};
}
}
return current;
});
}), ts),
{}
)
);
// Compare progress state to original milestones to determine completion
const areMilestonesCompleted = deepEqual(finalGoal, milestonesState);
// If completion happened, trigger callback
useEffect(() => {
if (areMilestonesCompleted) {
completionCallback();
}
}, [areMilestonesCompleted, completionCallback]);
// Return boolean flag for if completion successful and triggers. Triggers come first because those will always be
// used whereas code may use completion state and/or completion callback so completion state could be ignored
return [triggers, areMilestonesCompleted];
};
//////// useConsumedUrlParams.ts ////////
import { useHistory } from 'react-router';
import type { Location } from 'history';
type CopiedLocationKeys = Partial<Pick<Location, 'pathname' | 'search' | 'hash'>>;
const copyLocationKeys = (location: Location): CopiedLocationKeys =>
(['pathname', 'search', 'hash'] as (keyof CopiedLocationKeys)[]).reduce<CopiedLocationKeys>(
(l, key) => (location[key] ? Object.assign(l, { [key]: location[key] }) : l),
{}
);
export const extractQueryParamsFromRouterLocation = (
location: Location,
paramList: string[] = []
): [Partial<Location>, Record<string, string>] => {
if (paramList.length === 0) {
return [copyLocationKeys(location), {}];
}
const params = new URLSearchParams(location.search);
const consumedParams = paramList.reduce<Record<string, string>>((result, param) => {
const value = params.get(param);
if (value !== null) {
params.delete(param);
return (result[param] = value, result);
}
return result;
}, {});
return [
{
...copyLocationKeys(location),
search: `?${params.toString()}`,
},
consumedParams,
];
};
type ParamsObject = Record<string, string>;
export const useConsumedUrlParams = (location: Location, paramsList: string[] = []): ParamsObject => {
const [locationWithoutParams, consumedParams] = extractQueryParamsFromRouterLocation(location, paramsList);
const history = useHistory();
const params = useConstant(consumedParams);
useEffectOnMounted(() => {
if (Object.keys(consumedParams).length) {
history.replace(locationWithoutParams);
}
});
return params;
};
export default useConsumedUrlParams;
import type { MutableRefObject } from 'react';
/** Global general-use and app types */
export type EmptyObject = Record<string, never>;
/**
* A base type to use for when you need a type that represents a JS object
*
* TS advises against using `{}` if you want to make a value as a JS object with no specific keys defined. It advises
* this type for that use case.
*/
export type PlainObject = Record<string, any>;
export type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
export type Nullish = null | undefined;
/**
* The type for a function that takes a value and returns true or false depending on whether the values passes a test
*
* This kind of function is used in `Array.filter()` and the `validate()` utility
*/
export type Predicate<A = any> = (x: A) => boolean;
/**
* Interface to extend from for functional components that don't accept `children`. To be used when defining props
* interface for a component that uses the `FC` type. `FC` type allows for `children` by default. It can be used on its
* own for a component that does not accept any other props, or the component's prop interface can extend from it.
*
* "Empty" is used in the name to mimic "empty elements" in HTML which are elements that don't take child elements.
*
* @see https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
*/
export interface EmptyComponent {
children?: never;
}
/**
* Given the `typeof` a JS object const, return a union type of all the values of the object
*
* <code>
* const MyEnum = {
* ONE: 'one',
* TWO: 'two',
* THREE: 'three',
* } as const;
*
* type MyEnumValues = Values<typeof MyEnum>; // 'one' | 'two' | 'three'
* </code>
*/
export type Values<T extends Record<string, unknown>> = T[keyof T];
/**
* Converts an object type where all keys are made optional except those listed in RequiredKeys which become required
*
* <code>
* type MyProps = {
* prop1: string;
* prop2: number;
* prop3: string[];
* prop4: OtherType[];
* }
*
* type Modified = PartialExcept<MyProps, 'prop1' | 'prop3'>;
* // type Modified = {
* prop1: string;
* prop2?: number;
* prop3: string[];
* prop4?: OtherType[];
* // }
* </code>
*/
export type PartialExcept<T, RequiredKeys extends keyof T> = Partial<Omit<T, RequiredKeys>> &
{
[K in RequiredKeys]: T[K];
};
/**
* Type to require that a List value is non-empty
*
* TS has shortcomings where when you use this type, you'll need to provide a hard-coded first entry when you might
* normally just `.map()` transform. You'll have to destructure the first and rest entries and manually use the first
* entry to create a concrete first result entry. Otherwise, the result of an `arr.map()` won't be able to satisfy this
* type.
*/
export type NonEmptyList<T> = [T, ...T[]];
/**
* Type for factory functions that define a default value for all keys and allow for partial overriding
*/
export type Factory<T> = (data?: Partial<T>) => T;
export type Factory<T, RequiredKeys extends keyof T = ''> = (data?: PartialExcept<T, RequiredKeys>) => T;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment