Skip to content

Commit

Permalink
[#56] v1.6.0: TS: Improves theme typing
Browse files Browse the repository at this point in the history
  • Loading branch information
birdofpreyru committed Jan 10, 2024
1 parent c234dee commit 807a58f
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 34 deletions.
12 changes: 4 additions & 8 deletions __tests__/ts/theme-schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@ import themeA from '../../jest/theme-a.scss';
import invalidTheme from '../../jest/invalid-theme.scss';
import themeWithExtraStyles from '../../jest/theme-with-extra-styles.scss';

const themeKeys = ['container', 'content'] as const;

type ComponentPropsT = {
theme: Theme & {
container?: string;
content?: string;
};
theme: Theme<typeof themeKeys>;
};

function Component({ theme }: ComponentPropsT) {
return JSON.stringify(theme, null, 2);
}

const ThemedComponent = themed(Component, 'Component', [
'container',
'content',
]);
const ThemedComponent = themed(Component, 'Component', themeKeys);

describe('Theme verification', () => {
test('No errors with the correct theme', () => {
Expand Down
8 changes: 4 additions & 4 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@docusaurus/core": "^3.1.0",
"@docusaurus/preset-classic": "^3.1.0",
"@dr.pogodin/react-themes": "file:../dr.pogodin-react-themes-1.5.2.tgz",
"@dr.pogodin/react-themes": "file:../dr.pogodin-react-themes-1.6.0.tgz",
"@mdx-js/react": "^3.0.0",
"@svgr/webpack": "^8.1.0",
"dayjs": "^1.11.10",
Expand Down
7 changes: 3 additions & 4 deletions jest/TestComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { type Theme } from '../src';

export type ComponentTheme = Theme & {
container?: string;
content?: string;
};
export const validThemeKeys = ['container', 'content'] as const;

export type ComponentTheme = Theme<typeof validThemeKeys>;

export type ComponentProps = {
children?: React.ReactNode;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dr.pogodin/react-themes",
"version": "1.5.2",
"version": "1.6.0",
"description": "UI theme composition with CSS Modules and React",
"main": "./build/node/index.js",
"source": "src/index.ts",
Expand Down
46 changes: 32 additions & 14 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,34 @@ import {
// Note: Support of custom specifity-manipulation classes in TypeScript is too
// cumbersome, thus although it remains a functional feature for pure JavaScript,
// the TypeScript assumes these classes are always "ad", "hoc", and "context".
export interface Theme {
export interface ThemeI {
ad: string;
hoc: string;
context: string;
}

export interface ThemeMap { [key: string]: Theme | undefined }
// NOTE: This may work only if Keys is an array of string literals,
// which allows us to deduce the union of valid keys as Keys[number].
// E.g. ("as const" is critical to make it work):
//
// const validKeys = ['a', 'b'] as const;
// type T = Theme<typeof validKeys>;
//
// as a safeguard, if not used correctly, the resulting type will be "never".
export type Theme<Keys extends readonly string[]> =
// NOTE: Here, if Keys has the correct type, Keys[number] will be a union of
// string literals (e.g. 'a' | 'b'), which is not extendable by string, thus
// the condition will enter its second branch. Otherwise, Keys[number] will
// evalute to just "string", which is extendable by "string", and the result
// will be "never" - this is our safeguard against incorrect use.
string extends Keys[number]
? never
: ThemeI & { [key in Keys[number]]?: string };

export interface ThemeMap { [key: string]: ThemeI | undefined }

export interface ThemeableComponentProps {
theme: Theme;
theme: ThemeI;
}

export interface ThemePropsMapper<
Expand Down Expand Up @@ -143,7 +161,7 @@ ThemeProvider.defaultProps = {
* @param tag Specifity tag(s).
* @return Composed theme.
*/
function compose<CustomTheme extends Theme>(
function compose<CustomTheme extends ThemeI>(
high: CustomTheme | undefined,
low: CustomTheme | undefined,
mode: COMPOSE,
Expand Down Expand Up @@ -188,7 +206,7 @@ interface RequireableValidator extends Validator {
}

function createThemeValidator<ComponentProps extends ThemeableComponentProps>(
themeSchema?: (keyof ComponentProps['theme'])[],
themeSchema?: readonly (keyof ComponentProps['theme'])[],
options: ThemedOptions<ComponentProps> = {},
) {
const { adhocTag = 'ad.hoc', contextTag = 'context' } = options;
Expand All @@ -207,7 +225,7 @@ function createThemeValidator<ComponentProps extends ThemeableComponentProps>(
propName: string,
name: string,
) => {
const theme: Theme = props[propName];
const theme: ThemeI = props[propName];
if (!theme) return null;

const errors: string[] = [];
Expand Down Expand Up @@ -292,7 +310,7 @@ function createThemeValidator<ComponentProps extends ThemeableComponentProps>(
*/
function themedImpl<ComponentProps extends ThemeableComponentProps>(
componentName: string,
themeSchema?: (keyof ComponentProps['theme'])[],
themeSchema?: readonly (keyof ComponentProps['theme'])[],
defaultTheme?: ComponentProps['theme'],
options: ThemedOptions<ComponentProps> = {},
) {
Expand Down Expand Up @@ -362,7 +380,7 @@ function themedImpl<ComponentProps extends ThemeableComponentProps>(

let adhocTheme = theme;
if (castTheme && theme) {
const castedTheme = {} as Theme;
const castedTheme = {} as ThemeI;
validThemeKeys.forEach((key) => {
const clazz: string = (theme as any)[key];
if (clazz) (castedTheme as any)[key] = clazz;
Expand Down Expand Up @@ -407,7 +425,7 @@ function themedImpl<ComponentProps extends ThemeableComponentProps>(
function themed<ComponentProps extends ThemeableComponentProps>(
componentName: string,

themeKeysOrDefaultTheme?: (keyof ComponentProps['theme'])[]
themeKeysOrDefaultTheme?: readonly (keyof ComponentProps['theme'])[]
| ComponentProps['theme'],

defaultThemeOrOptions?: ComponentProps['theme']
Expand All @@ -420,7 +438,7 @@ function themed<ComponentProps extends ThemeableComponentProps>(
component: React.ComponentType<ComponentProps>,
componentName: string,

themeKeysOrDefaultTheme?: (keyof ComponentProps['theme'])[]
themeKeysOrDefaultTheme?: readonly (keyof ComponentProps['theme'])[]
| ComponentProps['theme'],

defaultThemeOrOptions?: ComponentProps['theme']
Expand All @@ -435,11 +453,11 @@ function themed<ComponentProps extends ThemeableComponentProps>(

// 2nd argument.
componentNameOrThemeKeysOrDefaultTheme?: string
| (keyof ComponentProps['theme'])[]
| readonly (keyof ComponentProps['theme'])[]
| ComponentProps['theme'],

// 3rd argument.
themeKeysOrDefaultThemeOrOptions?: (keyof ComponentProps['theme'])[]
themeKeysOrDefaultThemeOrOptions?: readonly (keyof ComponentProps['theme'])[]
| ComponentProps['theme']
| ThemedOptions<ComponentProps>,

Expand All @@ -456,7 +474,7 @@ function themed<ComponentProps extends ThemeableComponentProps>(
let component: React.ComponentType<ComponentProps> | undefined;
let componentName: string;
let defaultTheme: ComponentProps['theme'] | undefined;
let themeKeys: (keyof ComponentProps['theme'])[] | undefined;
let themeKeys: readonly (keyof ComponentProps['theme'])[] | undefined;
let ops: OpsT | undefined;

if (typeof componentOrComponentName === 'string') {
Expand All @@ -475,7 +493,7 @@ function themed<ComponentProps extends ThemeableComponentProps>(
} else if (typeof componentNameOrThemeKeysOrDefaultTheme === 'string') {
throw Error('Second argument is not expected to be a string');
} else {
defaultTheme = componentNameOrThemeKeysOrDefaultTheme;
defaultTheme = componentNameOrThemeKeysOrDefaultTheme as ComponentProps['theme'];

// 3rd argument: options.
ops = themeKeysOrDefaultThemeOrOptions as OpsT;
Expand Down

0 comments on commit 807a58f

Please sign in to comment.