根据输入缩小类型和类型变体

Narrowing down types and types variants based on Input

首先,我需要编写一个函数,根据函数的输入对象缩小类型。这个 Article 帮了我大忙,但现在我还需要根据输入缩小泛型类型的范围,经过几个小时的尝试后我无法让它工作

这是我给出的简化项目打字稿:

const ToolType = {
  TOOL_A: 'toolA',
  TOOL_B: 'toolB',
  TOOL_C: 'toolC',
} as const;

type ToolType = typeof ToolType[keyof typeof ToolType];

const ToolAVariants = {
  Variant_A : 'VariantA',
  Variant_B : 'VariantB',
  Variant_C : 'VariantC',
} as const

type ToolAVariants = typeof ToolAVariants[keyof typeof ToolAVariants];

type VariantASettings = {
  setting: 'A'
  type: typeof ToolAVariants.Variant_A
}
type VariantBSettings = {
  setting: 'B'
  type: typeof ToolAVariants.Variant_B
}
type VariantCSettings = {
  setting: 'C'
  type: typeof ToolAVariants.Variant_C
}
type VariantSettings = VariantASettings | VariantBSettings | VariantCSettings

type ToolA<T extends ToolAVariants> = {
  type: typeof ToolType.TOOL_A
  width: number
  Settings: ExtractActionParametersBase<VariantSettings, T>
}
type ToolB = {
  type: typeof ToolType.TOOL_B,
  height: string

}
type ToolC = {
  type: typeof ToolType.TOOL_C
  length: any[]

type MyToolTypes = ToolA<typeof ToolAVariants.Variant_A> | ToolA<typeof ToolAVariants.Variant_B> | ToolA<typeof ToolAVariants.Variant_C> | ToolB | ToolC

使用文章中的实用程序类型

type ExtractActionParameters<A, T> = A extends { type: T } ? A : never

出于 UI 目的,我还需要像这样向工具添加 ìd

type ToolIdWrapper<T extends MyToolTypes> = T & { id: string }

这是函数:

function createTool<T extends MyToolTypes['type'], Z extends VariantSettings['type']>(args: ExtractActionParameters<MyToolTypes, T>): ToolIdWrapper<typeof args> {
  return {
    id: 'someid',
    ...args
  }
}

如果我创建一个没有“变体”的工具,它工作正常:

const MyToolB_Wrapped: ToolIdWrapper<ToolB> = createTool({
  height: 'qwe',
  type: "toolB",
}) // -> no error

但尝试创建 ToolA 变体时,设置 属性 未正确缩小范围,仍然是联合类型

const MyToolA_Wrapped:ToolIdWrapper<ToolA<typeof ToolAVariants.Variant_A>> = createTool({
  width: 123,
  type: 'toolA',
  Settings: {
    setting: "A",
    type: "VariantA"
  }
}) // Error 

抛出此 TS 错误:Type 'ToolIdWrapper<ToolA<"VariantA"> | ToolA<"VariantB"> | ToolA<"VariantC">>' is not assignable to type 'ToolIdWrapper<ToolA<"VariantA">>

我基本上需要一个在这种情况下和其他情况下都不会抛出错误的函数类型

我试过像这样扩展实用程序类型

type ExtractActionParameters<A, T> = A extends { type: T } ? A extends {Settings: VariantSettings} ? A | {Settings: A['Settings']} : A : never

或带有条件扩展的 if/else-ing 的其他变体,但我无法让它工作。

为了更好的调试,我创建了这个 typescript playground

我猜这是一堵太大的文字墙,难以理解,但我终于找到了一个有效的解决方案。

首先,非常感谢 ,它帮助我解决了这个问题

回顾

目标是根据输入结构定义一个 returns 特定类型的函数。困难在于针对输入中的 属性 的嵌套类型区分,这并非在所有输入中都存在。

例如我们得到了TypeATypeBTypeC,其中TypeA只得到了一个额外的属性 TypeA.Variant可以进一步分化为TypeA<VariantA>TypeA<VariantB>TypeA<VariantC>

解决方案

const ToolType = {
  TOOL_A: 'toolA',
  TOOL_B: 'toolB',
  TOOL_C: 'toolC',
} as const;

type ToolType = typeof ToolType[keyof typeof ToolType];

const ToolAVariants = {
  Variant_A : 'VariantA',
  Variant_B : 'VariantB',
  Variant_C : 'VariantC',
} as const

type ToolAVariants = typeof ToolAVariants[keyof typeof ToolAVariants];


// A
type ToolA<T extends ToolAVariants> = {
  type: typeof ToolType.TOOL_A
  width: number
  Settings: T extends typeof ToolAVariants.Variant_A ? VariantASettings : T extends typeof ToolAVariants.Variant_B ? VariantBSettings : VariantCSettings
}
// A Variants in Object
type VariantASettings = {
  setting: 'A'
  type: typeof ToolAVariants.Variant_A
}
type VariantBSettings = {
  setting: 'B'
  type: typeof ToolAVariants.Variant_B
}
type VariantCSettings = {
  setting: 'C'
  type: typeof ToolAVariants.Variant_C
}

// B
type ToolB = {
  type: typeof ToolType.TOOL_B,
  height: string
}
// C
type ToolC = {
  type: typeof ToolType.TOOL_C
  length: any[]
}

// Union of all possible Tools
type MyToolTypes = ToolB | ToolC | ToolA<typeof ToolAVariants.Variant_A> | ToolA<typeof ToolAVariants.Variant_B> | ToolA<typeof ToolAVariants.Variant_C>

// Idx<T, K> looks up the K property of T, or returns never 
type Idx<T, K extends keyof any> = K extends keyof T ? T[K] : never;

// DiscriminateBy<U, K, T> takes a union type U, a key type K, and a candidate
// type T, and narrows U to all those whose values at the K property match that
// of T.  This might end up returning never, if there is no match.
type DiscriminateBy<U, K extends keyof any, T> = U extends any ? Idx<T, K> extends Idx<U, K> ? U : never : never

//  (in my eyes) TS Magic - Use DiscriminateBy to extract the types, we want to discriminate against in a nested fashion
type DiscriminateTypeAndSettings<E> = E extends MyToolTypes ? E : DiscriminateBy<DiscriminateBy<MyToolTypes, "type", E>, "Settings", E>


type IDWrapper<T> = T & { id: string }
function creatTool<E>(e: DiscriminateTypeAndSettings<E>): IDWrapper<DiscriminateTypeAndSettings<E>> {
    return {
      id:'someId',
      ...e
      };
}


// Works!
const A_VariantA:IDWrapper<ToolA<typeof ToolAVariants.Variant_A>> = creatTool({
  type: 'toolA',
  width: 123,
  Settings: {
    setting: "A",
    type: 'VariantA'
  },
})

// Error as Intended! -> Setting needs to be {setting:'A', type: 'VariantC'}
const A_VariantA_TRAP:IDWrapper<ToolA<typeof ToolAVariants.Variant_A>> = creatTool({
  type: 'toolA',
  width: 123,
  Settings: {
    setting: "C",
    type: 'VariantC'
  },
})

// Error as Intended! -> setting needs to be "B"
const A_VariantB:IDWrapper<ToolA<typeof ToolAVariants.Variant_B>> = creatTool({
  type: 'toolA',
  width: 123,
  Settings: {
    setting: "C",
    type: 'VariantB'
  },
})

最酷的东西 是的,我什至可以制作 Variants 的“子类型”并添加另一层 DiscriminateBy - 实用类型无限

虽然我不是很明白 DiscriminateBy 类型是如何工作的,但我很高兴它能工作