【问题标题】:Composition of typescript generics打字稿泛型的组成
【发布时间】:2021-06-04 16:33:13
【问题描述】:

我正在尝试在 typescript 中创建一个域建模系统,该系统受到 Scott Wlaschin 基于 F# 的 Domain Modeling Made Functional 的强烈影响。

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

// 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# 想法保持一致),因此所有类型都被声明为“类型” ' 没有接口或类*

【问题讨论】:

  • 听起来你想要更高级的类型(在你是通用的方式上是通用的)。 github.com/pelotom/hkts 在 TypeScript 中呈现 HKT 的编码。
  • 感谢@LeviRamsey!这看起来确实很有帮助。第一次阅读时,大约 99.99% 的内容超出了我的想象,但我会在睡个好觉后明天再看一遍 :) 似乎我需要更流利地使用功能术语,这不是坏事。
  • 您能解释一下您认为Input &amp; Record&lt;Tag, never&gt; 类型的作用吗?您是希望在此处验证Tag,还是您实际上的意思是您想要stringRecord(映射对象)的交集?
  • @evelynhathaway 这个想法是在编译时保护一个原始类型,以便可以区分多个字符串。在下面的示例中,我们可以将交易名称或帐户名称传递给函数,因为它在这里指定的只是一个字符串。 const accountName = 'Account A'const dealName = 'Deal A'type UseDealName = (name: string) =&gt; void
  • 这意味着可能会意外传递错误的东西。通过使用{[index: string]: never}“标记”string 类型,我们实际上是在为字符串类型添加一个不可见的注释,作为区分字符串的一种方式。由于我们使用 never 作为值类型,编译器从不期望看到它,并且变量在运行时仍被视为字符串,但我们可以锁定函数,以便它们在编译时只接受正确的“字符串类型” .

标签: typescript generics domain-driven-design composition


【解决方案1】:

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

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

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

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

string | number | boolean 来自SimpleInput 的约束,string 来自SimpleTag 的约束。

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

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

问题在于Record&lt;K, V&gt;{ [key in K]: V } 的简写,这是定义索引签名的语法。然而string(或其他原始类型)不能被索引。所以我不相信你在这里的品牌推广方法会奏效。

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

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

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

Playground

【讨论】:

  • 我 99% 确定您是绝对正确的,谢谢!绝对的英雄。我想在标签中使用“从不”来明确表明这是一个人为的结构,因为它有点像黑客,但如果它不起作用,那么它就不起作用。我只是想根据您的建议重新构建我的解决方案,如果可行,我会将您的解决方案标记为解决方案,如果您不介意添加一些关于我最终得到的细节的编辑。
猜你喜欢
  • 1970-01-01
  • 2015-11-27
  • 2021-11-26
  • 2022-01-04
  • 1970-01-01
  • 1970-01-01
  • 2018-11-17
  • 2021-12-20
  • 2022-08-21
相关资源
最近更新 更多