My Goal:
Imagine a configuration with types structured like this:
type ExmapleConfig = {
A: { Component: (props: { type: "a"; a: number; b: number }) => null };
B: { Component: (props: { type: "b"; a: string; c: number }) => null };
C: { Component: () => null };
};
Essentially, something like this:
type AdditionalConfigProps = {
additionalConfigProp?: string;
// + more additional props that are not optional
};
type ReservedComponentProps = {
reservedComponentProp: string;
};
type ComponentProps = ReservedComponentProps & Record<string, any>;
type Config = {
[key: string]: {
Component: (props: PropsShape) => JSX.Element;
} & AdditionalConfigProps;
};
I aim to transform a configuration like this, ensuring:
- The hard types for keys are preserved (
'A' | 'B' | 'C'
instead ofstring
) - The hard types for props are preserved (
instead of{ type: "a"; a: number; b: number }
Record<string, any>
) - The transform function only accepts valid configurations:
- It must have a
Component
property, and all other properties fromAdditionalConfigProps
must have correct types - It will not accept any additional properties beyond the defined
Component
and those inAdditionalConfigProps
- The
Component
function must be able to accept an object similar toComponentProps
as its first argument
- It must have a
The transformation process may look like this:
const config = {
A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> };
B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div> };
C: { Component: () => <div>abc</div> };
};
/*
It will extract Components and wrap them with an additional function
to return void instead of JSX
*/
const transformedConfig = transformConfig(config);
// typeof transformedConfig
type ResultType = {
A: (props: { type: "a"; a: number; b: number }) => void;
B: (props: { type: "b"; a: string; c: number }) => void;
C: () => void;
};
Note that:
- Hard types for keys 'A' | 'B' | 'C' were preserved
- Hard types for 'props' were preserved
My Attempted Approach:
import React from "react";
type AdditionalConfigProps = {
additionalConfigProp?: string;
};
type ReservedComponentProps = {
reservedComponentProp: string;
};
const CORRECT_CONFIG = {
A: {
Component: (props: { type: "a"; a: number; b: number }) => null,
additionalConfigProp: "abc"
},
B: { Component: (props: { type: "b"; a: string; c: number }) => null },
C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null },
D: { Component: (props: {}) => null },
E: { Component: () => null }
};
const BAD_CONFIG = {
// Missing Component or other required config prop
A: {},
// Incorrect additionalConfigProp
B: { Component: () => null, additionalConfigProp: 123 },
// Invalid Component
C: { Component: 123 },
// Incorrect component props type
D: { Component: (props: boolean) => null },
// Unexpected 'unknownProp'
E: { Component: () => null, unknownProp: 123 },
// Invalid 'reservedProp'
F: { Component: (props: { reservedProp: number }) => null }
};
function configParser<
Keys extends string,
ComponentPropsMap extends {
[Key in Keys]: ReservedComponentProps & Record<string, any>;
}
>(config: {
[Key in Keys]: {
Component: (props?: ComponentPropsMap[Keys]) => React.ReactNode;
} & AdditionalConfigProps;
}) {
/*
TODO: Transform config.
For now, we want to ensure that TS can handle it correctly.
*/
return config;
}
/*
❌ Throws unexpected type error
*/
const result = configParser(CORRECT_CONFIG);
// Expected typeof result (what I'd want)
type ExpectedResultType = {
A: {
Component: (props: { type: "a"; a: number; b: number }) => null;
additionalConfigProp: "abc";
};
B: { Component: (props: { type: "b"; a: string; c: number }) => null };
C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null };
D: { Component: (props: {}) => null };
E: { Component: () => null };
};
/*
❌ Should throw type errors, but not the ones it does
*/
configParser(BAD_CONFIG);
One possible solution could be:
function configParser<
Config extends {
[key: string]: {
Component: (componentProps: any) => React.ReactNode;
};
}
>(config: Config) {
return config;
}
// No type error, result type as expected
const result = configParser(CORRECT_CONFIG);
However, this approach:
- Does not validate
componentProps
(maybe
would, but for some reason, it doesn't acceptcomponentProps: Record<string, any> & ReservedComponentProps
CORRECT_CONFIG
) - Allows any additional config properties