Let's say that there is a config with type 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 };
};
so, generally speaking something of shape like this:
type AdditionalConfigProps = {
additionalConfigProp?: string;
// + more additional props that don't have to be optional
};
type ReservedComponentProps = {
reservedComponentProp: string;
};
type ComponentProps = ReservedComponentProps & Record<string, any>;
type Config = {
[key: string]: {
Component: (props: PropsShape) => JSX.Element;
} & AdditionalConfigProps;
};
I'd like to transform a config like this, but:
'A' | 'B' | 'C'
instead of string
){ type: "a"; a: number; b: number }
instead of Record<string, any>
)Component
property, and all other properties from AdditionalConfigProps
with correct types,Component
and the ones in AdditionalConfigProps
,Component
function has to be able to accept ComponentProps
-like object as first argument, Transformation 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> };
};
/*
Let's say that it will extract Components, and wrap them
with additional function so void will be returned 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;
};
Please notice that:
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: {},
// Bad additionalConfigProp
B: { Component: () => null, additionalConfigProp: 123 },
// Bad Component
C: { Component: 123 },
// Bad component props type
D: { Component: (props: boolean) => null },
// Unexpected 'unknownProp'
E: { Component: () => null, unknownProp: 123 },
// Bad '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 make sure that TS is even able to 'see' 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);
Of course I could do something like this:
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);
but it:
componentProps
(maybe componentProps: Record<string, any> & ReservedComponentProps
would, but for some reason it wouldn't accept CORRECT_CONFIG
)Here's one possible approach:
type VerifyConfigElement<T extends AdditionalConfigProps &
{ Component: (props: any) => void }> =
{ [K in Exclude<keyof T, "Component" | keyof AdditionalConfigProps>]: never } &
{
Component: (
props: Parameters<T["Component"]>[0] extends ComponentProps ? any : ComponentProps
) => void
}
declare function transformConfig<
T extends Record<keyof T, AdditionalConfigProps & { Component: (props: any) => void }>>(
config: T & { [K in keyof T]: VerifyConfigElement<T[K]> }
): { [K in keyof T]: (...args: Parameters<T[K]["Component"]>) => void }
The idea is to:
transformConfig()
generic in the type T
of the config
parameter;T
to a relatively easy-to-write type that doesn't reject good inputs, in this case it's AdditionalConfigProps & {Component: (props: any) => void}>
;T
more thoroughly, by mapping it from itself T[K]
to a related type VerifyConfigElement<T[K]>
where T[K] extends VerifyConfigElement<T[K]>
if and only if it's a good input;T
, by mapping each property of T
into a function type whose parameters are determined by indexing into the corresponding Component
property. The VerifyConfigElement<T>
type checks two things:
T
does not have any properties not explicitly mentioned in AdditionalConfigProps
(or "Component"
, of course)... it does this by mapping any such extra properties to have a never
type, which will almost certainly fail to type check;T
's Component
method's first parameter is assignable to ComponentProps
... it does this by mapping to any
if so (which will succeed) and ComponentProps
if not (which will probably fail? function types are contravariant in their input parameters, so there might be some edge cases here). Let's test it:
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> }
};
// typeof transformedConfig
type ResultType = {
A: (props: { type: "a"; a: number; b: number }) => void;
B: (props: { type: "b"; a: string; c: number }) => void;
C: () => void;
};////
const transformedConfig: ResultType = transformConfig(config);
Looks good! And for your CORRECT_CONFIG
and BAD_CONFIG
the compiler accepts and rejects them, respectively:
const okay = transformConfig(CORRECT_CONFIG); // okay
const bad = transformConfig(BAD_CONFIG); // error
As desired.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.