如何使用类型级别的函数动态创建静态类型? [英] How to use type-level functions to create static types, dynamically?

查看:42
本文介绍了如何使用类型级别的函数动态创建静态类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在TypeScript中,有 type-level 个功能,它们允许根据给定的 literal 类型/规格创建新类型.em>(请参见映射类型条件类型等).

例如,这是一个功能,可以说是由lib作者提供的:

  type FromSpec< S>= {[S的key中的K]:S [K]扩展了"foo"?ExampleType:从不}; 

其目的是,给定规范 S 以字符串键和任意文字的映射的形式,它以映射的形式创建具有映射键的新类型,并具有相同的键集和价值观转变了.如果值是文字"foo" ,则它将变为类型 ExampleType ,否则,将其转换为底部类型 never .

然后,最终用户可以按照上述说明使用此功能来创建新类型:

  type Example = FromSpec< {some_key:"foo",another_key:"bar"}>//= {some_key:ExampleType,another_key:从不} 

值得注意的是,lib作者并不知道给定最终用户可能需要哪种确切类型,因此为他提供了创建所需类型的功能.另一方面,最终用户只要遵守该功能的功能,就可以创建无限数量的新类型.

您可以在这里玩这个简单的例子,.


问题在于这种动态性"如何在其他类型的语言(例如,ReasonML/OCaml,Scala,Haskell)中表达.或者,作为最终用户,如何使用lib作者提供的类型级别的函数在编译时创建新类型(就像通常在运行时使用值级别的那样)功能)?

重要的是要注意的问题不是关于哪种语言更好,等等.这是关于找到表达这种能力的最直接,最明确的方式.在这里,我们看到了TypeScript中的一个示例,但是在其他任何语言中,还有自然种方式吗?

给出Scala是标记语言之一,这是Dotty(又名Scala 3)中的解决方案.由于Dotty仍在开发中,因此请带一点盐.经过Dotty版本0.24.0-RC1的测试,这是证明它可以实际编译的Scastie .

Scala没有与用于处理记录的TypeScript相同的内置类型机器.不用担心,我们可以自己滚!

 导入导出._//一个字段实际上只是一个字段名称和值的元组类型Field [K,V] =(K,V)//这仅有助于在正确的地方进行类型推断来推断单例类型def field [K< ;:带单例的字符串,V< ;:单例](标签:K,值:V):字段[K,V] =标签->价值//这是一些记录的示例val myRec1 =()val myRec2 = field("key1","foo")*:field("key2","foo")*:()val myRec3 =field("key1",1)*:field("key2","foo")*:field("key3","hello world")*:() 

然后,可以使用 //可以定义为有用的-`trait`只是在其中引入新类型的一种简便方法特质ExampleTypeval exampleValue = new ExampleType {}类型FromSpec [S< ;:元组]< ;:元组= S匹配{case Field [k,"foo"] *:rest =>字段[k,ExampleType] *:FromSpec [rest]case Field [k,v] *:rest =>字段[k,无] *:FromSpec [rest]case单位=>单元}

最后,让我们使用 FromSpec :

  def myRec1Spec:FromSpec [myRec1.type] =()def myRec2Spec:FromSpec [myRec2.type] =field("key1",exampleValue)*:field("key2",exampleValue)*:()def myRec3Spec:FromSpec [myRec3.type] = ???//没有非歧义的实现 

In TypeScript, there are type-level functions that allow creating new types based on given literal types/specifications (see Mapped Types, Conditional Types, etc.).

For instance, here is such a function, let say provided by a lib author:

type FromSpec<S> = { 
  [K in keyof S]: S[K] extends "foo" ? ExampleType : never 
};

Its purpose is, given a specification S in the form of a map of string keys and arbitrary literals, it creates a new type in the form of a map with the same set of keys and with values transformed. If a the value is the literal "foo" then it becomes the type ExampleType, otherwise the value is rejected by transforming it into the bottom type never.

Then, an end-user can make use of this function to create new types following the above explanation:

type Example = FromSpec<{some_key: "foo", another_key: "bar"}>
//           = {some_key: ExampleType, another_key: never} 

It's noteworthy that the lib author doesn't know about what exact type a given end-user may want, and thus provides him with a function to create the ones he needs. On the other hand, the end-user can create an infinite set of new types as long as he complies with the function's capabilities.

You can play around this simple example, here.


The question is about how this kind of "dynamism" is expressible in other typed languages (e.g., ReasonML/OCaml, Scala, Haskell). Or how, as an end-user, to create new types, at compile-time, by using type-level functions, provided by a lib author (as one would usually do at runtime with value-level functions)?

It's important to note that the question is not about which language is better, etc. It's about finding the most straightforward and explicit way to express such capabilities. Here we saw an example in TypeScript, but is there any more natural way in any other language?

解决方案

Given Scala is one of the tagged languages, here is a solution in Dotty (aka. Scala 3). Take this with a grain of salt, since Dotty is still under development. Tested with Dotty version 0.24.0-RC1, here is a Scastie that proves this actually compiles.

Scala doesn't have the same sort of built-in type machinery as TypeScript for manipulating records. Not to fear, we can roll our own!

import deriving._

// A field is literally just a tuple of field name and value
type Field[K, V] = (K, V)

// This just helps type-inference infer singleton types in the right places
def field[K <: String with Singleton, V <: Singleton](
  label: K,
  value: V
): Field[K, V] = label -> value

// Here is an example of some records
val myRec1 = ()
val myRec2 = field("key1", "foo") *: field("key2", "foo") *: () 
val myRec3 =
  field("key1", 1) *: field("key2", "foo") *: field("key3", "hello world") *: ()

Then, FromSpec can be implemented using a match-type. The never type in TypeScript is called Nothing in Scala/Dotty.

// Could be defined to be useful - `trait` is just an easy way to bring a new type in 
trait ExampleType
val exampleValue = new ExampleType {}

type FromSpec[S <: Tuple] <: Tuple = S match {
  case Field[k, "foo"] *: rest => Field[k, ExampleType] *: FromSpec[rest]
  case Field[k, v] *: rest => Field[k, Nothing] *: FromSpec[rest]
  case Unit => Unit
}

Finally, let's use FromSpec:

def myRec1Spec: FromSpec[myRec1.type] = ()
def myRec2Spec: FromSpec[myRec2.type] =
  field("key1", exampleValue) *: field("key2", exampleValue) *: () 
def myRec3Spec: FromSpec[myRec3.type] = ??? // no non-diverging implementation

这篇关于如何使用类型级别的函数动态创建静态类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆