Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stronger type safety for createRestyleFunction #21

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;