打字稿泛型的组成

Dan CloudFabriq

我正在尝试在打字稿中创建一个域建模系统,受到 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

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

问题是实体定义无效,因为 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# 想法保持一致),因此所有类型都被声明为“类型”,没有接口或类*

亚历克斯·韦恩

通用类型“简单”需要 2 个类型参数。

您始终必须为泛型类型提供泛型参数。唯一的例外是当这些通用参数具有默认值时,但情况并非如此。

您可以做的是传入这些泛型参数的原始约束,以此来表达“我不想在这里进一步限制泛型参数”

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

string | number | boolean来自Simple's的约束Inputstring来自Simple's的约束Tag

现在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)

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

问题是 aRecord<K, V>是 的简写{ [key in K]: V },它是定义索引签名的语法。然而string(或其他原始类型)不能被索引。所以我不相信你在这里建立品牌的方法会奏效。

相反,如果您Simple使用已知密钥进行标记,则不需要索引签名,一切都应该正常工作。

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

这应该同样安全,因为您无法string & { tag: 'foo' }在运行时真正创建 a

操场

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章