[英]TypeScript generics, constraint for function paramters in object
假設有一個類型如下的配置:
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 };
};
所以,一般來說,形狀像這樣:
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;
};
我想像這樣轉換配置,但是:
'A' | 'B' | 'C'
而不是string
){ type: "a"; a: number; b: number }
而不是Record<string, any>
)Component
屬性,以及AdditionalConfigProps
中具有正確類型的所有其他屬性,Component
和AdditionalConfigProps
中的任何其他屬性,Component
function 必須能夠接受ComponentProps
-like object 作為第一個參數,轉換可能如下所示:
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;
};
請注意:
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);
當然我可以做這樣的事情:
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);
但它:
componentProps
(也許componentProps: Record<string, any> & ReservedComponentProps
會,但由於某種原因它不會接受CORRECT_CONFIG
)這是一種可能的方法:
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 }
這個想法是:
config
參數的類型T
中使transformConfig()
泛型;T
約束為一個相對容易編寫的類型,它不會拒絕好的輸入,在這種情況下,它是AdditionalConfigProps & {Component: (props: any) => void}>
;T[K]
映射到相關類型VerifyConfigElement<T[K]>
來更徹底地檢查推斷T
的每個屬性,其中T[K] extends VerifyConfigElement<T[K]>
當且僅當它是一個好的輸入;T
計算返回類型,方法是將T
的每個屬性映射到 function 類型,其參數通過索引到相應的Component
屬性來確定。 VerifyConfigElement<T>
類型檢查兩件事:
T
沒有在AdditionalConfigProps
(或"Component"
,當然)中沒有明確提及的任何屬性......它通過將任何此類額外屬性映射為具有never
類型來做到這一點,這幾乎肯定會無法進行類型檢查;T
的Component
方法的第一個參數可以分配給ComponentProps
...它通過映射到any
如果是這樣(這將成功)和ComponentProps
如果不是(這可能會失敗?function 類型在其輸入參數中是逆變的,所以有這里可能是一些邊緣情況)。讓我們測試一下:
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);
看起來不錯! 對於您的CORRECT_CONFIG
和BAD_CONFIG
,編譯器分別接受和拒絕它們:
const okay = transformConfig(CORRECT_CONFIG); // okay
const bad = transformConfig(BAD_CONFIG); // error
如預期的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.