[英]How do you form a conditional type of optional (or partial) properties in Typescript?
使用相同 object 的可选/必需道具的不同组合,您如何形成将基于这些道具进行切换的条件类型?
在这个例子中,我试图在 function 中获取config
类型,只有当它们没有在defaultConfig
中设置时才会返回具有“必需”道具:
type Length = `${number}px`
type EmptyObject = Record<string, never>
// Optional props
type Optional = Partial<{
style: 'italic' | 'normal'
weight: number
lineHeight: number
}>
// Required props
type Size = {
size: Length
}
type Family = {
family: string
}
type DefaultConfig = EmptyObject | (Size & Family) | Size | Family
type Config<T extends DefaultConfig> = T extends Size & Family
? Partial<Size & Family>
: T extends Size
? Partial<Size> & Family
: T extends Family
? Size & Partial<Family>
: Size & Family
const makeFontShorthandGenerator = <T extends DefaultConfig>(
defaultConfig: T & Optional
): ((config: Config<T> & Optional) => string) => {
return (config): string => {
const cf = { ...defaultConfig, ...config }
// build and return font string
}
}
// Examples
const genFont1 = makeFontShorthandGenerator({ size: '16px', family: 'sans-serif' })
genFont1({}) // nothing required
const genFont2 = makeFontShorthandGenerator({ size: '16px' })
genFont2({ family: 'sans-serif' }) // requires 'family', but 'size' is still optional
const genFont3 = makeFontShorthandGenerator({})
genFont3({}) // this is where the problem is
现在,最后一个示例的预期类型是((Size & Family) | Partial<Size & Family> | (Partial<Size> & Family) | (Size & Partial<Family>)) & Partial<...>
,它允许genFont({})
这不是想要的。
我认为您可以在以下条件下处理它:
参考:
defaultConfig
是传递给外部 function 的参数。config
是传递给返回的 function 的参数。伪代码:
如果defaultConfig
为空,则config
必须具有所有必需的属性。
其他config
必须至少包含defaultConfig
中省略的所需道具。
config
可以选择包含在初始道具接口中定义的任何道具。 通过这种方式,可以将属性添加到可以扩展和/或覆盖defaultConfig
中的属性的config
中。
代码:
注意:我为更强大的测试示例添加了额外的必需道具color
:
interface Props {
size: `${number}px`;
family: string;
color: string;
style?: 'italic' | 'normal';
weight?: number;
lineHeight?: number;
}
type EmptyObject = {
[K in any]: never;
}
type DefaultConfig = Partial<Props>;
type AllowedConfig<T> = Omit<Props, keyof T> & DefaultConfig;
type Config<T> =
T extends EmptyObject
? Props
: AllowedConfig<T>
const makeFontShorthandGenerator = <T extends DefaultConfig>(
defaultConfig: T
) => (config: Config<T>): string => {
const cf = {
...defaultConfig,
...config
}
// build and return font string
return 'some string'
}
// Examples
const genFont1 = makeFontShorthandGenerator({ size: '16px', family: 'sans-serif', color: 'red' }) // T fits type `DefaultConfig`
genFont1({}) // nothing required
const genFont2 = makeFontShorthandGenerator({ size: '16px' }) // T fits type `DefaultConfig`
genFont2({ family: 'sans-serif', color: '#fff' }) // `AllowedConfig<T> ` required, because T is type `DefaultConfig`
const genFont3 = makeFontShorthandGenerator({}) // T is type `EmptyObject`
genFont3({}) // `Props` required, because T is type `EmptyObject
我倾向于使用单一类型的Config
代表完整配置 object,包括可选属性:
interface Config {
style?: 'italic' | 'normal',
weight?: number,
lineHeight?: number,
size: Length,
family: string
}
type Length = `${number}px`
然后,您的makeFontShorthandGenerator
应该接受一个defaultConfig
object ,其中包含来自Config
的一些属性子集。 这必须是通用的 function 以便编译器可以跟踪已设置的属性。 如果我们说defaultConfig
的键是一些通用类型K extends keyof Config
,那么defaultConfig
应该是Pick<Config, K>
类型,使用Pick
实用程序 type 。
makeFontShorthandGenerator()
的返回类型是 function ,它接受config
object ,该配置需要defaultConfig
未提供的部分Config
(此类型为Omit<Config, K>
,使用Omit
实用程序类型),并允许来自Config
的任何属性all(此类型是使用Partial
实用程序类型的Partial<Config>
)。 由于我们想要这两者, defaultConfig
的类型是相关类型的交集: Omit<Config, K> & Partial<Config>
::
const makeFontShorthandGenerator = <K extends keyof Config>(
defaultConfig: Pick<Config, K>
): ((config: Omit<Config, K> & Partial<Config>) => string) => {
return (config): string => {
const cf = { ...defaultConfig, ...config } as Config;
return "whatever"; // do the real implementation here
}
}
在实现内部,我使用了类型断言来表示cf
是Config
类型。 通用 object 传播导致交叉,因此编译器可以看到cf
的类型为Partial<Config> & Pick<Config, K> & Omit<Config, K>
。 除非出现极端情况,否则这应该等同于Config
。 不幸的是,编译器无法看到这种等价性,因为它需要一些编译器缺乏的高阶推理能力。 有关详细信息,请参阅microsoft/TypeScript#28884 。 结果是,如果我们希望编译器将cf
视为Config
,我们必须自己声明它。
最后,让我们确保它有效:
const genFont1 = makeFontShorthandGenerator({
size: '16px',
family: 'sans-serif',
})
genFont1({}) // okay, nothing required
const genFont2 = makeFontShorthandGenerator({ size: '16px' })
genFont2({ family: 'sans-serif', size: '14px' }) // okay
const genFont3 = makeFontShorthandGenerator({})
genFont3({}) // error!
/* Argument of type '{}' is not assignable to parameter of
type 'Omit<Config, never> & Partial<Config>'.
Type '{}' is missing the following properties from type
'Omit<Config, never>': size, family */
这看起来像你想要的行为。
最后一件事:错误消息提到Omit<Config, never> & Partial<Config>
。 如果你想要一个更明显的类型,你可以稍微改变makeFontShorthandGenerator
的类型签名,使用这个问题的答案中的自定义Expand
实用程序类型:
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
const makeFontShorthandGenerator = <K extends keyof Config>(
defaultConfig: Pick<Config, K>
): ((config: Expand<Omit<Config, K> & Partial<Config>>) => string) => {
return (config): string => {
const cf = { ...defaultConfig, ...config } as any as Config;
return "whatever";
}
}
const genFont3 = makeFontShorthandGenerator({})
genFont3({}) // error!
/* Argument of type '{}' is not assignable to parameter of type
'{ style?: "italic" | "normal" | undefined; weight?: number | undefined;
lineHeight?: number | undefined; size: `${number}px`; family: string; }'.
*/
现在错误消息提到了一个匿名类型(它恰好等同于Config
,因为defaultConfig
是{}
)具有拼写出来的属性,而不是实用程序类型的交集。
在@jcalz 评论的帮助下,这是一个答案:
解释:
因为Pick<RequiredProps, K>
是defaultConfig
中的道具的类型,所以在该类型上使用Omit
和Partial
可以为config
提供正确的类型。
type Length = `${number}px`
interface OptionalProps {
style?: 'italic' | 'normal'
weight?: number
lineHeight?: number
}
interface RequiredProps {
family: string
size: Length
}
const makeFontShorthandGenerator = <K extends keyof RequiredProps>(
defaultConfig: Pick<RequiredProps, K> & OptionalProps
): ((
config: Omit<RequiredProps, K> & Partial<Pick<RequiredProps, K>> & OptionalProps
) => string) => {
return (config): string => {
const cf = { ...defaultConfig, ...config } as RequiredProps & OptionalProps
return "whatever"
}
}
const genFont1 = makeFontShorthandGenerator({
size: '16px',
family: 'sans-serif',
})
genFont1({}) // nothing required
const genFont2 = makeFontShorthandGenerator({ size: '16px' })
genFont2({ family: 'sans-serif', size: '14px' }) // requires 'family', but 'size' is still optional
const genFont3 = makeFontShorthandGenerator({})
genFont3({}) // error!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.