Skip to content

Commit

Permalink
Stronger type safety for createRestyleFunction (#21)
Browse files Browse the repository at this point in the history
* Improve type-safety by using generics

* Improve TypeSafety of useRestyle hook

This ensures that the output props from useRestyle are actually typed properly instead of casting them to `any`

* Cleanup / improve typings of useRestyle and RestyleFunction

* Strong type safety for createRestyleFunction

Improve the typings of createRestyleFunction, e.g. the prop value in transform will be strongly typed/inferred.

Also added some other improvements, like removing the need for explicit return value on useRestyle that was actually
throwing away the "style" prop in the returned type. This was done by properly typing filterRestyleProps so everything
could be inferred.

* Fix type error in createRestyleFunction.test.ts
  • Loading branch information
META-DREAMER authored Jul 22, 2020
1 parent b7d40d4 commit ad59d33
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/createRestyleComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {BaseTheme, RestyleFunctionContainer} from './types';
import useRestyle from './hooks/useRestyle';

const createRestyleComponent = <
Props extends Record<string, unknown>,
Props extends Record<string, any>,
Theme extends BaseTheme = BaseTheme
>(
restyleFunctions: (
Expand Down
78 changes: 49 additions & 29 deletions src/createRestyleFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,54 @@ import {
ResponsiveValue,
BaseTheme,
Dimensions,
RNStyle,
RestyleFunctionContainer,
} from './types';
import {getKeys} from './typeHelpers';

type PropValue = string | number | undefined | null;

type StyleTransformFunction<
Theme extends BaseTheme,
K extends keyof Theme | undefined
> = (params: {value: any; theme: Theme; themeKey?: K}) => any;
K extends keyof Theme | undefined,
TVal
> = (params: {value: TVal | null; theme: Theme; themeKey?: K}) => TVal | null;

const getValueForScreenSize = ({
const getValueForScreenSize = <Theme extends BaseTheme, TVal>({
responsiveValue,
breakpoints,
dimensions,
}: {
responsiveValue: {[key in keyof BaseTheme['breakpoints']]: any};
breakpoints: BaseTheme['breakpoints'];
responsiveValue: {[key in keyof Theme['breakpoints']]?: TVal};
breakpoints: Theme['breakpoints'];
dimensions: Dimensions;
}) => {
}): TVal | null => {
const sortedBreakpoints = Object.entries(breakpoints).sort((valA, valB) => {
return valA[1] - valB[1];
});
const {width} = dimensions;
return sortedBreakpoints.reduce((acc, [breakpoint, minWidth]) => {
if (width >= minWidth && responsiveValue[breakpoint] !== undefined)
return responsiveValue[breakpoint];
return acc;
}, null);
return sortedBreakpoints.reduce<TVal | null>(
(acc, [breakpoint, minWidth]) => {
if (width >= minWidth && responsiveValue[breakpoint] !== undefined)
return responsiveValue[breakpoint] as TVal;
return acc;
},
null,
);
};

const isResponsiveObjectValue = <Theme extends BaseTheme>(
val: ResponsiveValue<any, Theme>,
const isResponsiveObjectValue = <Theme extends BaseTheme, TVal>(
val: ResponsiveValue<TVal, Theme>,
theme: Theme,
): val is {[key: string]: any} => {
): val is {[Key in keyof Theme['breakpoints']]?: TVal} => {
if (!val) return false;
if (typeof val !== 'object') return false;
return Object.keys(val).reduce((acc: boolean, key) => {
return acc && theme.breakpoints[key] !== undefined;
return getKeys(val).reduce((acc: boolean, key) => {
return acc && theme.breakpoints[key as string] !== undefined;
}, true);
};

type PropValue = string | number | undefined | null;
type ValueOf<T> = T[keyof T];

function isThemeKey<Theme extends BaseTheme>(
theme: Theme,
Expand All @@ -49,20 +58,28 @@ function isThemeKey<Theme extends BaseTheme>(
return theme[K as keyof Theme];
}

const getValue = <Theme extends BaseTheme, K extends keyof Theme | undefined>(
propValue: ResponsiveValue<PropValue, Theme>,
const getValue = <
TVal extends PropValue,
Theme extends BaseTheme,
K extends keyof Theme | undefined
>(
propValue: ResponsiveValue<TVal, Theme>,
{
theme,
transform,
dimensions,
themeKey,
}: {
theme: Theme;
transform?: StyleTransformFunction<Theme, K>;
transform?: StyleTransformFunction<Theme, K, TVal>;
dimensions: Dimensions;
themeKey?: K;
},
): PropValue => {
):
| TVal
| (K extends keyof Theme ? ValueOf<Theme[K]> : never)
| null
| undefined => {
const val = isResponsiveObjectValue(propValue, theme)
? getValueForScreenSize({
responsiveValue: propValue,
Expand All @@ -72,45 +89,48 @@ const getValue = <Theme extends BaseTheme, K extends keyof Theme | undefined>(
: propValue;
if (transform) return transform({value: val, theme, themeKey});
if (isThemeKey(theme, themeKey)) {
if (val && theme[themeKey][val] === undefined)
if (val && theme[themeKey][val as string] === undefined)
throw new Error(`Value '${val}' does not exist in theme['${themeKey}']`);

return val ? theme[themeKey][val] : val;
return val ? theme[themeKey][val as string] : val;
}

return val;
};

const createRestyleFunction = <
Theme extends BaseTheme = BaseTheme,
TProps extends Record<string, unknown> = Record<string, unknown>,
TProps extends Record<string, any> = Record<string, any>,
P extends keyof TProps = keyof TProps,
K extends keyof Theme | undefined = undefined
>({
property,
transform,
styleProperty = property.toString(),
styleProperty,
themeKey,
}: {
property: P;
transform?: StyleTransformFunction<Theme, K>;
styleProperty?: string;
transform?: StyleTransformFunction<Theme, K, TProps[P]>;
styleProperty?: keyof RNStyle;
themeKey?: K;
}): RestyleFunctionContainer<TProps, Theme, P, K> => {
const styleProp = styleProperty || property.toString();

return {
property,
themeKey,
variant: false,
func: (props, {theme, dimensions}) => {
const value = getValue(props[property] as PropValue, {
const value = getValue(props[property], {
theme,
dimensions,
themeKey,
transform,
});
if (value === undefined) return {};

return {
[styleProperty]: value,
[styleProp]: value,
};
},
};
Expand Down
10 changes: 8 additions & 2 deletions src/createVariant.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {BaseTheme, ResponsiveValue, RestyleFunctionContainer} from './types';
import {
BaseTheme,
ResponsiveValue,
RestyleFunctionContainer,
RNStyle,
} from './types';
import createRestyleFunction from './createRestyleFunction';
import {all, AllProps} from './restyleFunctions';
import composeRestyleFunctions from './composeRestyleFunctions';
Expand Down Expand Up @@ -39,9 +44,10 @@ function createVariant<
}): RestyleFunctionContainer<TProps, Theme, P, K> {
const styleFunction = createRestyleFunction<Theme, TProps, P, K>({
property,
styleProperty: 'expandedProps',
styleProperty: 'expandedProps' as keyof RNStyle,
themeKey,
});

return {
property,
themeKey,
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useRestyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const useRestyle = <
| RestyleFunctionContainer<TRestyleProps, Theme>
| RestyleFunctionContainer<TRestyleProps, Theme>[])[],
props: TProps,
): Omit<TProps, keyof TRestyleProps> => {
) => {
const theme = useTheme<Theme>();

const dimensions = useDimensions();
Expand Down
3 changes: 1 addition & 2 deletions src/restyleFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {TextStyle, FlexStyle, ViewStyle} from 'react-native';

import createRestyleFunction from './createRestyleFunction';
import {BaseTheme, ResponsiveValue} from './types';
import {getKeys} from './typeHelpers';

const spacingProperties = {
margin: true,
Expand Down Expand Up @@ -99,8 +100,6 @@ const textShadowProperties = {
textShadowRadius: true,
};

const getKeys = <T>(object: T) => Object.keys(object) as (keyof T)[];

export const backgroundColor = createRestyleFunction({
property: 'backgroundColor',
themeKey: 'colors',
Expand Down
3 changes: 2 additions & 1 deletion src/test/createRestyleFunction.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import createRestyleFunction from '../createRestyleFunction';
import {RNStyle} from '../types';

const theme = {
colors: {},
Expand Down Expand Up @@ -32,7 +33,7 @@ describe('createRestyleFunction', () => {
it('allows configuring the style object output key', () => {
const styleFunc = createRestyleFunction({
property: 'opacity',
styleProperty: 'testOpacity',
styleProperty: 'testOpacity' as keyof RNStyle,
});
expect(styleFunc.func({opacity: 0.5}, {theme, dimensions})).toStrictEqual(
{
Expand Down
1 change: 1 addition & 0 deletions src/typeHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getKeys = <T>(object: T) => Object.keys(object) as (keyof T)[];
9 changes: 6 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ export interface RestyleFunctionContainer<
}

export type RestyleFunction<
TProps extends Record<string, unknown> = Record<string, unknown>,
Theme extends BaseTheme = BaseTheme
TProps extends Record<string, any> = Record<string, any>,
Theme extends BaseTheme = BaseTheme,
TVal = any
> = (
props: TProps,
context: {theme: Theme; dimensions: Dimensions},
) => Record<string, any>;
) => {
[key in string]?: TVal;
};

export type RNStyle = ViewStyle | TextStyle | ImageStyle;

0 comments on commit ad59d33

Please sign in to comment.