From 297cd1479b5ce977f26df3c1102f1a4132ed8181 Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Fri, 26 Jun 2020 00:47:11 -0600 Subject: [PATCH 1/5] Improve type-safety by using generics --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index cc82bf1c..8b9a32ed 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,7 @@ export interface RestyleFunctionContainer< } export type RestyleFunction< - TProps extends Record = Record, + TProps extends Record = Record, Theme extends BaseTheme = BaseTheme > = ( props: TProps, From ba0b75f92e4a54a210abb6f58670d8f6bbe6e248 Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Tue, 30 Jun 2020 21:47:09 -0600 Subject: [PATCH 2/5] Improve TypeSafety of useRestyle hook This ensures that the output props from useRestyle are actually typed properly instead of casting them to `any` --- src/hooks/useRestyle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useRestyle.ts b/src/hooks/useRestyle.ts index 808d7c50..5bc56341 100644 --- a/src/hooks/useRestyle.ts +++ b/src/hooks/useRestyle.ts @@ -44,12 +44,12 @@ const useRestyle = < const dimensions = useDimensions(); const composedRestyleFunction = useMemo( - () => composeRestyleFunctions(restyleFunctions), + () => composeRestyleFunctions(restyleFunctions), [restyleFunctions], ); const style = composedRestyleFunction.buildStyle(props, {theme, dimensions}); - const cleanProps = filterRestyleProps( + const cleanProps = filterRestyleProps( props, composedRestyleFunction.properties, ); From 5711f9afc3c9aab13d064274db03beb6df53fddd Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Wed, 1 Jul 2020 03:29:01 -0600 Subject: [PATCH 3/5] Cleanup / improve typings of useRestyle and RestyleFunction --- src/hooks/useRestyle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useRestyle.ts b/src/hooks/useRestyle.ts index 5bc56341..808d7c50 100644 --- a/src/hooks/useRestyle.ts +++ b/src/hooks/useRestyle.ts @@ -44,12 +44,12 @@ const useRestyle = < const dimensions = useDimensions(); const composedRestyleFunction = useMemo( - () => composeRestyleFunctions(restyleFunctions), + () => composeRestyleFunctions(restyleFunctions), [restyleFunctions], ); const style = composedRestyleFunction.buildStyle(props, {theme, dimensions}); - const cleanProps = filterRestyleProps( + const cleanProps = filterRestyleProps( props, composedRestyleFunction.properties, ); From 64043b309766981e6a55440b243eeb38f1026cba Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Thu, 16 Jul 2020 21:05:08 -0600 Subject: [PATCH 4/5] 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. --- src/createRestyleComponent.tsx | 2 +- src/createRestyleFunction.ts | 78 +++++++++++++++++++++------------- src/createVariant.ts | 10 ++++- src/hooks/useRestyle.ts | 2 +- src/restyleFunctions.ts | 3 +- src/typeHelpers.ts | 1 + src/types.ts | 7 ++- 7 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 src/typeHelpers.ts diff --git a/src/createRestyleComponent.tsx b/src/createRestyleComponent.tsx index 1ab41c1e..6ff5c9a8 100644 --- a/src/createRestyleComponent.tsx +++ b/src/createRestyleComponent.tsx @@ -5,7 +5,7 @@ import {BaseTheme, RestyleFunctionContainer} from './types'; import useRestyle from './hooks/useRestyle'; const createRestyleComponent = < - Props extends Record, + Props extends Record, Theme extends BaseTheme = BaseTheme >( restyleFunctions: ( diff --git a/src/createRestyleFunction.ts b/src/createRestyleFunction.ts index 24aafcd8..40b50f42 100644 --- a/src/createRestyleFunction.ts +++ b/src/createRestyleFunction.ts @@ -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 = ({ 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( + (acc, [breakpoint, minWidth]) => { + if (width >= minWidth && responsiveValue[breakpoint] !== undefined) + return responsiveValue[breakpoint] as TVal; + return acc; + }, + null, + ); }; -const isResponsiveObjectValue = ( - val: ResponsiveValue, +const isResponsiveObjectValue = ( + val: ResponsiveValue, 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[keyof T]; function isThemeKey( theme: Theme, @@ -49,8 +58,12 @@ function isThemeKey( return theme[K as keyof Theme]; } -const getValue = ( - propValue: ResponsiveValue, +const getValue = < + TVal extends PropValue, + Theme extends BaseTheme, + K extends keyof Theme | undefined +>( + propValue: ResponsiveValue, { theme, transform, @@ -58,11 +71,15 @@ const getValue = ( themeKey, }: { theme: Theme; - transform?: StyleTransformFunction; + transform?: StyleTransformFunction; dimensions: Dimensions; themeKey?: K; }, -): PropValue => { +): + | TVal + | (K extends keyof Theme ? ValueOf : never) + | null + | undefined => { const val = isResponsiveObjectValue(propValue, theme) ? getValueForScreenSize({ responsiveValue: propValue, @@ -72,10 +89,10 @@ const getValue = ( : 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; @@ -83,34 +100,37 @@ const getValue = ( const createRestyleFunction = < Theme extends BaseTheme = BaseTheme, - TProps extends Record = Record, + TProps extends Record = Record, P extends keyof TProps = keyof TProps, K extends keyof Theme | undefined = undefined >({ property, transform, - styleProperty = property.toString(), + styleProperty, themeKey, }: { property: P; - transform?: StyleTransformFunction; - styleProperty?: string; + transform?: StyleTransformFunction; + styleProperty?: keyof RNStyle; themeKey?: K; }): RestyleFunctionContainer => { + 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, }; }, }; diff --git a/src/createVariant.ts b/src/createVariant.ts index a10a46a4..40754bcb 100644 --- a/src/createVariant.ts +++ b/src/createVariant.ts @@ -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'; @@ -39,9 +44,10 @@ function createVariant< }): RestyleFunctionContainer { const styleFunction = createRestyleFunction({ property, - styleProperty: 'expandedProps', + styleProperty: 'expandedProps' as keyof RNStyle, themeKey, }); + return { property, themeKey, diff --git a/src/hooks/useRestyle.ts b/src/hooks/useRestyle.ts index 808d7c50..dae7b065 100644 --- a/src/hooks/useRestyle.ts +++ b/src/hooks/useRestyle.ts @@ -38,7 +38,7 @@ const useRestyle = < | RestyleFunctionContainer | RestyleFunctionContainer[])[], props: TProps, -): Omit => { +) => { const theme = useTheme(); const dimensions = useDimensions(); diff --git a/src/restyleFunctions.ts b/src/restyleFunctions.ts index dc11d82c..330e1b8a 100644 --- a/src/restyleFunctions.ts +++ b/src/restyleFunctions.ts @@ -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, @@ -99,8 +100,6 @@ const textShadowProperties = { textShadowRadius: true, }; -const getKeys = (object: T) => Object.keys(object) as (keyof T)[]; - export const backgroundColor = createRestyleFunction({ property: 'backgroundColor', themeKey: 'colors', diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts new file mode 100644 index 00000000..88de497f --- /dev/null +++ b/src/typeHelpers.ts @@ -0,0 +1 @@ +export const getKeys = (object: T) => Object.keys(object) as (keyof T)[]; diff --git a/src/types.ts b/src/types.ts index 8b9a32ed..2b226cd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,10 +42,13 @@ export interface RestyleFunctionContainer< export type RestyleFunction< TProps extends Record = Record, - Theme extends BaseTheme = BaseTheme + Theme extends BaseTheme = BaseTheme, + TVal = any > = ( props: TProps, context: {theme: Theme; dimensions: Dimensions}, -) => Record; +) => { + [key in string]?: TVal; +}; export type RNStyle = ViewStyle | TextStyle | ImageStyle; From f82ace3b7721af49afb7e675ce517e74163eb748 Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Thu, 16 Jul 2020 21:20:54 -0600 Subject: [PATCH 5/5] Fix type error in createRestyleFunction.test.ts --- src/test/createRestyleFunction.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/createRestyleFunction.test.ts b/src/test/createRestyleFunction.test.ts index f8f9dce2..ca168076 100644 --- a/src/test/createRestyleFunction.test.ts +++ b/src/test/createRestyleFunction.test.ts @@ -1,4 +1,5 @@ import createRestyleFunction from '../createRestyleFunction'; +import {RNStyle} from '../types'; const theme = { colors: {}, @@ -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( {