打字稿泛型的组成

Composition of typescript generics

我正在尝试用 typescript 创建一个域建模系统,受到 Scott Wlaschin 的 域建模功能化 的强烈影响,它基于 F#。

我无法找到处理泛型属性传递的正确方法,因此泛型对象类型可以将 属性 指定为另一种泛型类型的某种形式,而无需立即强制解析.很难用文字解释,所以这里有一个代码示例,大致说明了我要实现的目标:

// We create a Simple generic so that we can
// prevent the direct use of primitives
// giving us an oppotunity to validate input (see make* fns below)
type Simple<
    Input extends
        | string
        | number
        | boolean,
    Tag extends string
> = Input & Record<Tag, never>

// We create an Id type which is a Simple string
type Id<Tag extends string> = Simple<string, Tag>

// We create an Entity type which accepts a
// string indexed interface with any Simple type as it's properties
// and a Tag which is passed down to lock the Id type
type Entity<
    Input extends {[index: string]: Simple},
    Tag extends string
> = Input & { id: Id<Tag> }

// We create a Deal type
// which is an Entity with an Id tagged with 'deal'
type Deal = Entity<{
    name: DealName
}, 'deal'>

// We define our Deal property types
type DealId = Id<'deal'>
type DealName = Simple<string, 'dealName'>

// we define Factories for creating
// our Deal properties and our Deals
// as the types are locked by the tags,
// this is now the only way to create them.
// This means once we have a Deal instance at run time,
// we know it has been validated
const makeDealId = (input: string) => {
    // validate deal id here
    return input as DealId
} 

const makeDealName = (input: string): DealName => {
    // validate deal name here
    return input as DealName
}

const makeDeal = (input: {
    id: DealId
    name: DealName
}): Deal => {
    // validate deal here
    return input as Deal
}

// Fails
const dealIdA: DealId = 'qwerty' // Type 'string' is not assignable to type 'DealId'
const dealNameA: DealName = 'Deal A' // Type 'string' is not assignable to type 'DealName'
const dealA: Deal = {
    id: dealIdA,
    name: dealNameA,
}

// Succeed
const dealIdB =  makeDealId('qwerty')
const dealNameB = makeDealName('Deal B')
const dealB: Deal = makeDeal({
    id: dealIdB,
    name: dealNameB,
})

// dealB is a valid Deal

*这是一个非常精简的版本,希望足以说明问题,但不包括嵌套的实体和值对象以及应用约束等。

问题是 Entity 定义无效,因为 Simple 是泛型,我们没有提供它的参数,所以我们得到这个错误:

Generic type 'Simple' requires 2 type argument(s).

然而在这一点上我们不关心我们采用什么形式的 Simple,只关心 属性 必须是某种 Simple 的东西,而不是字符串 |编号 |布尔值,或任何其他...

我试过类似的东西:

type Entity<
    Input extends {[index: string]: Simple<unknown>},
    Tag extends string
> = Input & { id: Id<Tag> }

甚至(尽管很脏):

type Entity<
    Input extends {[index: string]: Simple<any>},
    Tag extends string
> = Input & { id: Id<Tag> }

我显然在概念上遗漏了一些东西。如果有人想尝试解决这个问题并为我指明正确的方向,我将不胜感激。

** 需要注意的一件事,无论好坏,我都在努力使它尽可能地发挥作用(因为我的大脑喜欢它,并且因为它有助于使其与 Scott 的 F# 思想保持一致),因此所有类型都已声明'types' 没有接口或 类*

Generic type 'Simple' requires 2 type argument(s).

您始终必须为泛型类型提供泛型参数。唯一的例外是当那些通用参数有默认值时,但这里不是这种情况。

你可以做的是传入那些泛型参数的原始约束,以此表示“我不想在这里进一步约束泛型参数”

type Entity<
    Input extends Record<string, Simple<string | number | boolean, string>>,
    Tag extends string
> = Input & { id: Id<Tag> }

string | number | boolean来自SimpleInput的约束,string来自Simple的[=19=的约束].

现在 Entity 可以接收将通过的更具体的类型。


缺点是你在两个地方有相同的约束,这可能很难维护。但这很容易用一个额外的类型别名来解决:

type SimpleInputConstraint = string | number | boolean

type Simple<
  Input extends SimpleInputConstraint,
  Tag extends string
> = //...

type Entity<
    Input extends Record<string, Simple<SimpleInputConstraint, string>>,
    Tag extends string
> = //...

但是,现在又出现了一个新问题:

type Deal = Entity<{
    name: Simple<string, 'dealName'>
}, 'deal'>
// Index signature is missing in type 'String & Record<"dealName", never>'.(2344)

免责声明:以下解释可能不正确,但已尽我所能理解。有时使用高级打字稿类型只是尝试不同的安排,直到打字稿喜欢它,并尽最大努力理解为什么它确实有效。

问题是 Record<K, V> 对于 { [key in K]: V } 是 shorthand,这是定义索引签名的语法。然而 string (或其他基本类型)不能这样被索引。所以我不相信你在这里的品牌推广方法会奏效。

相反,如果您使用已知密钥标记 Simple,则不需要索引签名,一切都应该有效。

type Simple<
    Input extends SimpleInputConstraint,
    Tag extends string
> = Input & { _tag: Tag }

这应该是同样安全的,因为你不能在运行时真正创建 string & { tag: 'foo' }

Playground