@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
88 lines (81 loc) • 4.28 kB
text/typescript
import {mergeCallback} from './mergeCallback';
export type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];
export type RemoveNulls<T> = Omit<T, KeysMatching<T, null>>;
/**
* MergeProps will merge keys from `U` over `T` except when the value of `T` is `null`.
*
* ```ts
* MergeProps<
* {foo: string, bar: string, baz: null},
* {foo: string, bar: number, baz: string}
* > // { foo: string, bar: number, baz: null }
* ```
*/
export type MergeProps<T, U> = {
// merge keys from both `T` and `U`
[K in keyof T | keyof U]: K extends keyof T // test if key is in `T`
? K extends keyof U // test if key is also in `U`
? T[K] extends null // test if `T[K]` is `null`
? null // `K` is in both `T` and `U` and `T[K]` is `null`, so return `null`
: U[K] // `K` is in both `T` and `U`, but isn't `null` in `T[K]`, so return `U[K]`
: T[K] // `K` is only in `T`, so return `T[K]`
: K extends keyof U // K is not in `T`, so test if it is in `U`. This should always match at this point, but there's no "else" in type ternaries
? U[K] // K is only in `U`, so return `U[K]`
: never; // We should never get here, but type ternaries need all paths defined. `never` is usually used in these cases
};
// This file suppresses TS errors that come from merging interfaces of elements that aren't
// determined within components. Element interfaces are determined only when used, so TS errors
// aren't even useful here. Things get complicated when merging interfaces of callbacks.
/**
* Merges source props into target props, overwriting target props that are also defined on source
* props. Callback will be merged in such a way that both callbacks will be called. `css` and
* `style` props will be merged in such a way that source props have the final override.
*
* If `targetProps` has a `null` set, it will remove the prop from the `sourceProps`. This allows
* passing of props from merged hooks to another without passing out to the final element props.
*/
export function mergeProps<const T extends object, const S extends object>(
targetProps: T,
sourceProps: S
): RemoveNulls<MergeProps<T, S>> {
const returnProps = {...targetProps} as MergeProps<T, S>;
for (const key in sourceProps) {
if (sourceProps.hasOwnProperty(key)) {
if (key === 'css' && targetProps.hasOwnProperty('css')) {
// @ts-ignore
const css = [].concat(targetProps[key], sourceProps[key]);
// @ts-ignore
returnProps[key] = css;
} else if (key === 'style' && targetProps.hasOwnProperty('style')) {
// merge style objects
// @ts-ignore TS complains that key might not be on targetProps, but typeof returns
returnProps[key] = {...targetProps[key], ...sourceProps[key]};
} else if (key === 'ref' && sourceProps.hasOwnProperty('ref')) {
// refs should never be overridden. This doesn't happen normally, but happens with hook
// composition
(returnProps as any).ref = (returnProps as any).ref || (sourceProps as any).ref;
} else if (
typeof sourceProps[key] === 'function' &&
// @ts-ignore TS complains that key might not be on targetProps, but typeof returns
// `undefined` in that case, so this is safe
typeof targetProps[key] === 'function'
) {
// @ts-ignore TS complains that T and S aren't compatible, but this isn't useful for us
returnProps[key] = mergeCallback(targetProps[key], sourceProps[key]);
} else if (
typeof sourceProps[key] === 'undefined' &&
// @ts-ignore T and S, blah, blah, blah
typeof targetProps[key] === 'function'
) {
// we don't support removing callbacks via `undefined`, so do nothing here
// @ts-ignore Typescript complains that key might not exist in `targetProps` since we're iterating over sourceProps. At runtime this doesn't matter
} else if (targetProps[key] === null) {
// target props is trying to disable the prop for whatever reason. Consider `null` a "remove this prop"
} else {
// @ts-ignore TS has more object constraint complaints that aren't useful here
returnProps[key] = sourceProps[key];
}
}
}
return returnProps;
}