如何在 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 中的道具类型,然后在该类型上使用 OmitPartial 给出 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 以获得更强的测试示例:

TS Playground

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 intersectionOmit<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{})具有拼写属性而不是实用程序类型的交集.


Playground link to code