【发布时间】: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 & Record<Tag, never>类型的作用吗?您是希望在此处验证Tag,还是您实际上的意思是您想要string和Record(映射对象)的交集? -
@evelynhathaway 这个想法是在编译时保护一个原始类型,以便可以区分多个字符串。在下面的示例中,我们可以将交易名称或帐户名称传递给函数,因为它在这里指定的只是一个字符串。
const accountName = 'Account A'const dealName = 'Deal A'type UseDealName = (name: string) => void -
这意味着可能会意外传递错误的东西。通过使用
{[index: string]: never}“标记”string类型,我们实际上是在为字符串类型添加一个不可见的注释,作为区分字符串的一种方式。由于我们使用 never 作为值类型,编译器从不期望看到它,并且变量在运行时仍被视为字符串,但我们可以锁定函数,以便它们在编译时只接受正确的“字符串类型” .
标签: typescript generics domain-driven-design composition