如何在 Typescript 中形成条件类型的可选(或部分)属性?
How do you form a conditional type of optional (or partial) properties in Typescript?
对于同一对象的 optional/required 个 props 的不同组合,如何形成一个根据这些 props 进行切换的条件类型?
在这个例子中,我试图在返回的函数中获取 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({})
这不是我们想要的。
在@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!
我认为你可以满足以下条件:
参考:
defaultConfig
是传递给外部函数的参数。
config
是传递给返回函数的参数。
伪代码:
如果defaultConfig
为空,config
必须具备所有必需的道具。
否则config
必须至少包含在defaultConfig
.
中省略的必需道具
config
可以选择包含初始定义的任何道具
道具界面。这样,可以将属性添加到 config
中,可以扩展 and/or 覆盖 defaultConfig
.
中的属性
代码:
注意:我添加了一个额外的必需道具 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
表示完整的配置对象,包括可选属性:
interface Config {
style?: 'italic' | 'normal',
weight?: number,
lineHeight?: number,
size: Length,
family: string
}
type Length = `${number}px`
然后,您的 makeFontShorthandGenerator
应该接受一个 defaultConfig
对象,其中包含来自 Config
的一些属性子集。这必须是 generic function so that the compiler can keep track of which properties have already been set. If we say that the keys of defaultConfig
are of some generic type K extends keyof Config
, and defaultConfig
should be of type Pick<Config, K>
, using the Pick
utility type.
makeFontShorthandGenerator()
的 return 类型是一个接受 config
对象的函数,该对象 需要 Config
的一部分未随 defaultConfig
提供(此类型为 Omit<Config, K>
,使用相关类型的 the Omit
utility type), and allow any property from Config
at all (this type is Partial<Config>
using the Partial
utility type). Since we want both of those, defaultConfig
's type is the intersection: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
}
}
在实现中,我使用了 type assertion to say that cf
is of type Config
. Generic object spreads result in intersections, and so the compiler can see that cf
is of type Partial<Config> & Pick<Config, K> & Omit<Config, K>
. Barring edge cases, this should be equivalent to just Config
. Unfortunately the compiler is unable to see such equivalence, as it requires some higher order reasoning capabilities the compiler lacks. See 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
是 {}
)具有拼写属性而不是实用程序类型的交集.
对于同一对象的 optional/required 个 props 的不同组合,如何形成一个根据这些 props 进行切换的条件类型?
在这个例子中,我试图在返回的函数中获取 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({})
这不是我们想要的。
在@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!
我认为你可以满足以下条件:
参考:
defaultConfig
是传递给外部函数的参数。config
是传递给返回函数的参数。
伪代码:
如果
defaultConfig
为空,config
必须具备所有必需的道具。否则
中省略的必需道具config
必须至少包含在defaultConfig
.
中的属性config
可以选择包含初始定义的任何道具 道具界面。这样,可以将属性添加到config
中,可以扩展 and/or 覆盖defaultConfig
.
代码:
注意:我添加了一个额外的必需道具 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
表示完整的配置对象,包括可选属性:
interface Config {
style?: 'italic' | 'normal',
weight?: number,
lineHeight?: number,
size: Length,
family: string
}
type Length = `${number}px`
然后,您的 makeFontShorthandGenerator
应该接受一个 defaultConfig
对象,其中包含来自 Config
的一些属性子集。这必须是 generic function so that the compiler can keep track of which properties have already been set. If we say that the keys of defaultConfig
are of some generic type K extends keyof Config
, and defaultConfig
should be of type Pick<Config, K>
, using the Pick
utility type.
makeFontShorthandGenerator()
的 return 类型是一个接受 config
对象的函数,该对象 需要 Config
的一部分未随 defaultConfig
提供(此类型为 Omit<Config, K>
,使用相关类型的 the Omit
utility type), and allow any property from Config
at all (this type is Partial<Config>
using the Partial
utility type). Since we want both of those, defaultConfig
's type is the intersection: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
}
}
在实现中,我使用了 type assertion to say that cf
is of type Config
. Generic object spreads result in intersections, and so the compiler can see that cf
is of type Partial<Config> & Pick<Config, K> & Omit<Config, K>
. Barring edge cases, this should be equivalent to just Config
. Unfortunately the compiler is unable to see such equivalence, as it requires some higher order reasoning capabilities the compiler lacks. See 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
是 {}
)具有拼写属性而不是实用程序类型的交集.