正确的IntelliSense功能,可接收并返回异构字典(TypeScript) [英] Proper IntelliSense for function that takes and returns heterogeneous dictionary (TypeScript)

查看:79
本文介绍了正确的IntelliSense功能,可接收并返回异构字典(TypeScript)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试为接收和返回异构类型的函数编写TypeScript类型定义,但是正在努力使编译器输出正确的类型.我希望精通此事的人能有所帮助.

I am trying to write a TypeScript type definition for a function that receives and returns heterogeneous types, but am struggling to get the compiler to output the right types. I am hoping someone well-versed in this could help.

假设我有两个类,即斑马和狗,定义如下:

Suppose I have two classes, a Zebra and a Dog, defined as follows:

declare class Zebra {
    constructor(id: number); 
    getDescription(): {
        "type": string;
        "height": number;
    }
}

declare class Dog {
    constructor(id: number);
    getDescription(): {
        "type": string;
        "tailWagSpeed": number;
        "dailyCupsOfFood": number;
    }
}

现在,假设我要为函数" fetchAnimalData "创建类型定义,该定义接受键值对字典(用于我要在一个网络中检索的数据)往返数据库).一个典型的呼叫可能如下所示:

Now, suppose I want to create a type definition for a function, "fetchAnimalData", which accepts a dictionary of key-value pairs (for data that I want to retrieve in one network roundtrip to a database). A typical call may look as follows:

let result = await fetchAnimalData({
    fido: new Dog(11),
    zeebs: new Zebra(12344)
});
let { zeebs, fido } = result;

console.log("Fido's tail-wagging speed is " + fido.tailWagSpeed);
console.log("Meanwhile, Zeebs' height is " + zeebs.height);

如您所见,fetchAndTransform函数采用键值对.键只是任意字符串(以后将用于向其分配变量).该值是类型的异类混合(尽管如果需要让它们从常见的Animal类继承,也可以).函数调用的结果应该是对相同键名但其值对应于"getDescription"的强类型字典的Promise.输入特定动物的 .

As you can see, the fetchAndTransform function takes key-value pairs. The key is just any arbitrary string (which will later be used for assigning variables to). The value is a heterogenous mix of types (though if we need to have them inherit from a common Animal class, that's fine). The result of the function call should be a Promise for a strongly-typed dictionary with the same key names, but with values that correspond to the "getDescription" type of that particular animal.

是否可以让TypeScript为此进行强类型推断?特别地,我想确保我看到针对"tailWagSpeed"的IntelliSense建议.当我键入"fido .__"时.

Is it possible to get TypeScript to do strongly-typed inferencing for this? In particular, I want to make sure I see the IntelliSense suggestion for "tailWagSpeed" when I type "fido.__".

PS:要获得奖励积分,请更进一步:如果我的值变成了Touple,该怎么办?:

PS: For bonus points, taking it one step further: what if I the values becomes a Touple?:

let result = await fetchAndTransform({

zebraData: [new Zebra(12344)],

dogTailSpeed: [new Dog(11), (description) => description.dogTailSpeed]

});

let { zebraData, dogTailSpeed } = result;

console.log("Dog 11's tail-wagging speed is " + dogTailSpeed);

console.log("Meanwhile, Zebra 12344's height is " + zebraData.height);

在此fetchAndTransform版本中,字典中每个项目的值现在是一个Touple,它带有一个动物,并且可选地是一个转换函数.转换函数接收类型正确的对象,并可以提取所需的特定数据.因此,fetchAndTransform的结果成为具有与以前相同的键的强类型字典,但是其值现在与"getDescription"相对应.特定动物的类型,或提取的数据.

In this fetchAndTransform version, the value of each item in the dictionary is now a Touple that takes an animal and optionally a transform function. The transform function receives the properly-strongly-typed object, and can extract out the specific data it needs. Thus, the result of fetchAndTransform becomes a strongly-typed dictionary with the same keys as before, but with values that now correspond either to the "getDescription" type for the particular animal, or to the extracted data.

如果可以使用TypeScript之一或两者都做,那么我将非常高兴学习如何做.谢谢!

If it's possible to do either or both in TypeScript, I would be absolutely delighted to learn how. Thanks!

更新1:感谢@jcalz的出色回答.我很好奇的一件事是,为什么TypeScript类型推断会在某个特定点停止.例如,假设我有以下情况(仍然无法回答加分点"问题,但尝试朝该方向前进)

UPDATE 1: Thanks to @jcalz for the awesome answer. One thing I'm curious about is why does TypeScript type inferencing stop at a particular point. For example, suppose I have the following (which still doesn't answer the "bonus points" question, but tries to head in that direction)

declare function fetchAnimalData2<T>(
    animalObject: { [K in keyof T]: [{ getDescription(): T[K] }, (input: T[K]) => any] }
): Promise<T>;

通过使fetchAnimalData2进入一个元组,该元组的第二个参数的类型为(input: T[K]) => any,我突然第二次使用T[K]覆盖了第一个.也就是说,如果我这样做:

By making fetchAnimalData2 take in an Tuple, whose type for the 2nd parameter is (input: T[K]) => any, I suddenly am having this second use of T[K] override the first. That is, if I do:

let result2 = await fetchAnimalData2({
    zebraZeebs: [new Zebra(134), (r) => 5]
});

然后result2.zebraZeebs.___返回一个any类型,因为它是由于我没有在(r) => 5表达式而不是new Zebra(123)表达式上键入而造成的.如果我想使场景正常运行,我实际上最终需要在调用方键入Tuple为(i: { type: string; height: number }) => ___,这达到了目的:

Then result2.zebraZeebs.___ returns an any type, because it went off of my lack of typing on the (r) => 5 expression instead of the new Zebra(123) expression). If I want the scenario to work right, I actually end up needing to type the Tuple at the calling side as (i: { type: string; height: number }) => ___, which kind of defats the purpose:

let result2 = await fetchAnimalData2({
    zebraZeebs: [new Zebra(134), (i: { type: string; height: number }) => 5]
});

TypeScript是否有方法优先于元组中的第一项而不是第二项?还是给编译器一个提示,说第二个参数是第一个参数的结果,或者类似的东西?...谢谢!

Is there a way for TypeScript to take precedence for the first item in the Tuple instead of the 2nd? Or provide a hint to the compiler, saying that the 2nd argument extends from the result of the first, or something like that?... Thanks!

更新2: @artem也有一个很棒的答案,部分原因是他质疑我的设计从一开始是否正确(元组确实是最有用的结构,而不是功能) ?).如果我要使用一个功能(在可用性方面还是一样好或更好),那么该语言将提供更多的灵活性.

UPDATE 2: @artem also had an awesome answer, in part because he questioned whether my design was correct to begin with (is a tuple really the most usable structure for this, instead of a function?). If I would go with a function -- which usability-wise is as good or better -- the language offers a lot more flexibility.

对于那些对我最终选择的内容感到好奇的人,这里是@artem答案的修改后的版本(对于我的用例而言有所简化).我仍然想拥有我的动物的实例(让我们把它们看作是代理动物,在数据库中代表真实的动物),并且我不需要实现代码,只需要声明,所以我只保留了 -s.因此,事不宜迟,即使我不知道自己想要什么,这里的答案也可能与我想要的答案最相似!

For those who are curious with what I ended up choosing, here is my modified (and somewhat simplified for my use-case) version of @artem's answer. I still wanted to have instances of my animals (let's think of them as proxy-animals, standing in for real ones in my database), and I didn't need the implementation code, just the declaration, so I only kept the declare-s. So, without further ado, here is a version of the answer that probably most closely resembles what I wanted, even if I didn't know that I wanted it!

interface AnimalRequest<D, Result = D> {
    _descriptionType: D[]; // use empty arrays just to encode the types
    _resultType: Result[];
    transform<R>(transform: (animal: D) => R): AnimalRequest<D, R>;
}

declare function request<D>(c: { getDescription(): D }): AnimalRequest<D>;

declare function fetchAnimalData<RM extends {[n in string]: AnimalRequest<{}>}>(rm: RM)
  : Promise<{[n in keyof RM]: RM[n]['_resultType'][0]}>;

async function test(): Promise<any> {
    const result = await fetchAnimalData({
        fido: request(new Dog(3)),
        zebraZeebsHeight: request(new Zebra(123)).transform(r => r.height)
    })
    let { fido, zebraZeebsHeight } = result;
    console.log(fido.tailWagSpeed);
    console.log(zebraZeebsHeight);
}

推荐答案

在获得任何奖励积分之前,我不妨尝试回答第一个问题.是的,可以使用映射类型映射类型的推断:

Before I go for any bonus points I might as well try to answer the first question. Yes, it's possible to get what you want, using mapped types and inference from mapped types:

declare function fetchAnimalData<T>(
  animalObject: {[K in keyof T]: { getDescription(): T[K] }}
): Promise<T>;

基本上将输出类型视为T(好吧,是Promise<T>),并想象哪种输入类型将起作用.它的某些键与T相同,但是对于每个键K,input属性应该是具有getDescription()方法的事物,该方法返回T[K].让我们看看它的作用:

Basically think of the output type as T (well, a Promise<T>), and imagine what kind of input type would work. It would be something with the same keys as T, but for each key K, the input property should be something with a getDescription() method that returns a T[K]. Let's see it in action:

async function somethingOrOther() {
  let result = await fetchAnimalData({
    fido: new Dog(11),
    zeebs: new Zebra(12344)
  });
  let { zeebs, fido } = result;

  console.log("Fido's tail-wagging speed is " + fido.tailWagSpeed);
  console.log("Meanwhile, Zeebs' height is " + zeebs.height);
}

有效!希望有帮助!

如果我想到如何使元组正常工作,我会通知您.

If I think of how to get the tuple thing working I'll let you know.

好吧,我想不出一种很好的方式来键入元组疯狂,这主要是因为它涉及到只是一个动物的一个元素元组(单个"?)的结合,还是一个包含两个元素的元组的结合.动物和转换函数的元组(我猜是成对"),其中转换函数的类型取决于对动物的描述的类型,这需要类似存在性的类型(根据要求提供更多信息)和TypeScript的推论在这里行不通.

Okay, I can't think of a great way to type that tuple craziness, mostly because it involves a union of either the one-element tuple ("single"?) of just an animal, or the two-element tuple ("pair", I guess) of an animal and a transformer function, where the type of the transformer function depends on the the type of the description of the animal, which requires something like existential types (more info upon request) and TypeScript's inference won't work here.

但是,让我介绍一下人工元组":

However, let me introduce the "artificial tuple":

function artificialTuple<D, V>(animal: { getDescription(): D }, transformer: (d: D) => V): { getDescription(): V } {
  return {
    getDescription: () => transformer(animal.getDescription())
  }
}

您传入了两个参数:动物和转换器函数,它吐出了一个已经被转换的新的伪动物.您可以通过旧的fetchAnimalData()函数像这样使用它:

You pass in two arguments: the animal, and the transformer function, and it spits out a new pseudo-animal which has already been transformed. You can use it like this, with the old fetchAnimalData() function:

async function somethingElse() {
  let result = await fetchAnimalData({
    zebraData: new Zebra(12344),
    // look, see a typo on the next line!  Thanks TS!
    dogTailSpeed: artificialTuple(new Dog(11), (description) => description.dogTailSpeed)
  });

  let { zebraData, dogTailSpeed } = result;
  console.log("Dog 11's tail-wagging speed is " + dogTailSpeed);
  console.log("Meanwhile, Zebra 12344's height is " + zebraData.height);
}

请注意TS如何捕获不是Dogdescription属性的dogTailSpeed错字.这是因为artificalTuple()函数要求transformer函数自变量作用于animal自变量的getDescription()结果.将description.dogTailSpeed更改为description.tailWagSpeed即可.

Note how TS catches the typo with dogTailSpeed not being a property of a Dog's description. That's because the artificalTuple() function requires that the transformer function argument act on the result of getDescription() from the animal argument. Change description.dogTailSpeed to description.tailWagSpeed and it works.

好的,我知道这不是您想要的精确,但是它工作得很好,并且可以与TypeScript很好地配合使用.不知道我是获得奖励积分还是获得参与奖.干杯!

Okay, I know this isn't precisely what you asked for, but it works just as well and plays nicely with TypeScript. Not sure if I get my bonus points or just a participation award. Cheers!

@MichaelZlatkovsky说:

@MichaelZlatkovsky said:

假设我有以下...
suppose I have the following...
declare function fetchAnimalData2(
    animalObject: { [K in keyof T]: [{ getDescription(): T[K] }, (input: T[K]) => any] }
): Promise;

啊,但是,如果我了解您希望返回类型是什么,那不是正确的类型. T的属性值应该是转换器函数的输出类型,对吗?因此,元组的第二个元素需要看起来像(input: ???) => T[K],而不是(input: T[K]) => ???.

Ah, but that's not the right type, if I understand what you want the return type to be. The values of the properties of T should be the output type of the transformer function, right? So the second element of the tuple needs to look like (input: ???) => T[K], not (input: T[K]) => ???.

因此我们必须将其更改为以下内容:

So we must change it to something like the following:

declare function fetchAnimalData2<T>(
    animalObject: { [K in keyof T]: [{ getDescription(): ??? }, (input: T[K]) => ???] }
): Promise<T>;

如果将???替换为any,则可以完成这种工作,但是您放弃了重要的限制,即转换函数应作用于动物的getDescription()方法返回的类型.这就是为什么我介绍了artificialTuple()的原因,该方法使用通用的V代替???并保证getDescription()方法的输出和转换器功能的输入是兼容的(并捕获了上面的错字).

This will kind of work if you replace ??? with any, but you lose the important restriction that the transformer function should act on the type returned by the animal's getDescription() method. That's why I introduced artificialTuple(), which uses generic V to replace ??? and guarantees that the getDescription() method output and the transformer function input are compatible (and caught that typo above).

您试图描述给TypeScript的类型是这样的:

The type that you are trying to describe to TypeScript is something like this:

declare function fetchAnimalData2<T, V>(
    animalObject: { [K in keyof T]: [{ getDescription(): V[K] }, (input: V[K]) => T[K]] }
): Promise<T>;

其中,V从键名到getDescription()输出类型的映射.但是TypeScript不知道如何从传入的animalObject值推断V,实际上,它变成{},并不比any好.

where V the mapping from key name to getDescription() output type. But TypeScript has no idea how to infer V from the passed in animalObject value, and in practice it becomes {} which is no better than any.

您真的不能比我的建议做得更好(我想除了将artificialTuple()的名称更改为类似transformAnimal()的名称之外).您想告诉TypeScript您不关心VV[K],只要有 some 类型有效,并且您不想指定它即可.这称为存在类型,并且很少有语言支持它.

You can't really do much better than my suggestion (other than changing artificialTuple()'s name to something like transformAnimal() I guess). You want to tell TypeScript that you don't care about V or V[K] as long as there's some type that works, and you don't want to specify it. That's called an existential type and few languages support it.

好吧,祝你好运!

这篇关于正确的IntelliSense功能,可接收并返回异构字典(TypeScript)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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