NodeJS 使TypeScript容器与键自动完成

2izufjch  于 2023-10-17  发布在  Node.js
关注(0)|答案(2)|浏览(124)

如何创建一个包含键值对的容器,允许包含自动完成键?

示例

class Container<T> {
....
}

const container = new Container<number>();

container.set('one', 1);
container.set('two', 2);
container.set('three', 3);

container.getAll().one // Auto-completion for 'one', 'two', 'three'
container.getAll().z // TypeScript error for accessing an undefined key

**注意:**容器只是我想创建的一个类的一个例子,它的行为与前面提到的一样。

还有,有没有一种方法可以从Map构造函数中获得键的联合?
例如:

type Containers = .... // 'One' | 'two' | 'three'

谢谢你,谢谢

lnvxswe2

lnvxswe21#

这在TypeScript中是不可能的,事实上在我所知道的任何其他语言中也是如此。在编译时,你所有的打字都被删除了。您希望获得在运行时添加到容器中的值的类型推断。TypeScript无法知道在运行时将向容器中添加哪些值,因此这里没有建议。
但是,你可以实现类似这样的东西:

注意:我强烈建议反对在真实的项目中使用此类技巧和技巧。这真的不是你想在你的代码中的东西,因为它基本上让TypeScript给予你错误的建议,这些建议可能会与存储在容器中的实际值不同。

我只是在实验的目的张贴:)
TSPlayground

class Container<T, Keys extends string = never> {
    private store: Record<Keys, T> = {} as Record<Keys, T>

    get(key: Keys): T {
        return this.store[key];
    }

    set<const K extends string>(key: K, val: T): Container<T, Keys | K> {
        (this.store as Record<Keys | K, T>) [key] = val;

        return this as Container<T, Keys | K>
    }

    getAll(): Record<Keys, T> {
        return this.store
    }
}

const unchained = new Container<number>()

// To make this work you should always chain your .set(...) , because if
// the chain breaks then types will be lost
const chained = unchained
    .set('age', 23)
    .set("bd", 2000)

chained.getAll() // <- here the inference works, and the values "age" and "bd" are actually in the Container#store property.

unchained.getAll() // <- here the inference does'nt work, although the values "age" and "bd" are still in the Container#store property.

/**
 * Extracts keys intersection type from a container
 */
type ContainerKeys<C> = C extends Container<any, infer T> ? T : unknown

/**
 * Here you can test the suggestions
 */
const key: ContainerKeys<typeof chained> = '' // <- here the suggestions work

一个更好的,干净的,可读的和可维护的方法是预定义你要使用的键,就像这样:

type Keys = 'age' | 'bd'

class Container<T> {
  private store: Record<Keys, T | undefined> = {} as Record<Keys, T | undefined>

  getAll(): Record<Keys, T | undefined> {...}

  set(key: Keys, val: T) {...}
}
cwtwac6a

cwtwac6a2#

我会这么做

// these infer types, extract the types I want by `infer` syntax
type InferContainersValue<TContainer extends Container<unknown>> = TContainer extends Container<infer T> ? T : never
type InferContainersName<TContainer extends Container<unknown>> = TContainer extends Container<any, infer T> ? T : never

class Container<TValue, TContainers extends string[] = []> {
  private containers = new Map()

  set<TKey extends string>(key: TKey, value: TValue) {
    // In here, I create a new instance from the class,
    // pass my new key type as generic and set the value

    const clone = new Container<TValue, [...TContainers, TKey]>()

    clone.containers = new Map(this.containers)
    clone.containers.set(key, value)

    return clone
  }

  getKeys(): InferContainersName<typeof this> {
    // code here
  }

  getValues(): InferContainersValue<typeof this>[] {
    // code here
  }
}

const container = new Container<number>()
  .set("id", 1)
  .set("age", 1)
  
const keys = container.getKeys() // ["id", "age"]
const values = container.getValues() // number[]

什么是infer?(前两行)

根据文件
我们发现自己使用条件类型来应用约束,然后提取类型。这最终成为一个如此常见的操作,条件类型使其更容易。
条件类型为我们提供了一种方法,可以使用income关键字从我们在真分支中比较的类型中进行推断。
我们可以把它看作是破坏。假设我们想在js中从数组中解构一个元素:

const [item] = arr

在类型世界中,我们可以使用infer从另一个类型中提取一个类型:

type Arr = [string, number, boolean]

type Item = Arr extends [infer T] ? T : never

// Item === string

在我们的代码中,我使用这种推断语法来提取我们传递给类的泛型类型,如下所示:

type InferContainersName<TContainer extends Container<unknown>> = TContainer extends Container<any, infer T> ? T : never

为什么每次设置值都要克隆类?

在我回答这个问题之前,你应该知道typescript中的类型系统并不关心代码在运行时发生了什么。因此,如果我们使用这种模式,类型系统将不会像预期的那样工作:

const container = new Container().set("one", 1)

container.getKeys() // return type: ["one"]

container.set("two", 2)
container.getKeys() // return type: ["one"]

这个问题的存在是因为类型脚本不能改变另一个变量/对象类型。它只能为新对象分配新值。因此,我们必须用新类型重新创建类并返回它。但是,为什么我不直接返回this并显式重写类型呢?想象一下这个例子:

const baseContainer = new Container<string>()
    .set("databaseUrl", "...")

const backendOneContainer = baseContainer
    .set("port", "3000")
const backendTwoContainer = baseContainer
    .set("port", "3001")

在这种情况下,如果我们不克隆示例,只返回this,那么只有一个容器示例会产生冲突:

  1. baseContainer,只有databaseUrl和端口3001
    但是如果我们每次设置一个新值时都克隆示例,那么在这个场景中,将有3个示例:
  2. baseContainer,只有databaseUrl
  3. backendOneContainer,具有数据库URL和端口3000
  4. backendTwoContainer,具有数据库URL和端口3001
    如果您对它感到困惑,或者需要为其添加更多功能,请随时提出问题

相关问题