繁体   English   中英

如何在 Typescript 中形成条件类型的可选(或部分)属性?

[英]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

TS游乐场

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
  }
}

在实现内部,我使用了类型断言来表示cfConfig类型。 通用 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{} )具有拼写出来的属性,而不是实用程序类型的交集。


Playground 代码链接

在@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!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM