适用于获取和返回异构字典的函数的IntelliSense(TypeScript)

时间:2017-10-06 01:04:10

标签: typescript intellisense mapped-types

我正在尝试为接收和返回异构类型的函数编写TypeScript类型定义,但我很难让编译器输出正确的类型。我希望有精通这个的人可以提供帮助。

假设我有两个类,Zebra和Dog,定义如下:

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 ”,它接受一个键值对的字典(对于我想在一个网络往返中检索的数据)一个数据库)。典型的呼叫可能如下所示:

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类,那就没问题)。函数调用的结果应该是具有相同键名的强类型字典的Promise,但其值与该特定动物的“getDescription”类型相对应

是否可以让TypeScript为此进行强类型推理?特别是,当我输入“fido .__”时,我想确保看到“tailWagSpeed”的IntelliSense建议。

  

PS:对于奖励积分,更进一步:如果我的值变成了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”类型相对应对于特定动物或提取的数据。

如果可以在TypeScript中执行其中一项或两项,我会非常高兴知道如何操作。谢谢!

更新1:感谢@jcalz的精彩回答。我很好奇的一件事是为什么TypeScript类型的推理在特定点停止。例如,假设我有以下(仍然没有回答“奖励积分”问题,但试图朝这个方向前进)

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]覆盖了第一个参数。也就是说,如果我这样做:

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

然后result2.zebraZeebs.___会返回any类型,因为它缺少我在(r) => 5表达式而不是new Zebra(123)表达式上的输入。如果我希望场景正常工作,我实际上最终需要在调用端键入元组(i: { type: string; height: number }) => ___,这样可以解决目的:

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

TypeScript有没有办法优先考虑元组中的第一项而不是第二项?或者向编译器提供一个提示,说第二个参数是从第一个的结果延伸出来的,还是类似的东西?...谢谢!

更新2: @artem也有一个很棒的答案,部分是因为他质疑我的设计是否正确开始(是一个元组真的是最有用的结构,而不是一个函数?)。如果我使用一个函数 - 可用性方面也是好的或更好的 - 语言提供了更大的灵活性。

对于那些对我最终选择的内容感到好奇的人,这里是我对@ artem的答案进行了修改(并且在某种程度上简化了我的用例)版本。我仍然希望拥有我的动物实例(我们将它们视为代理动物,在我的数据库中代表真实的动物),我不需要实现代码,只需要声明,所以我只保留{{ 1}} - 秒。所以,不用多说,这里的答案版本可能最接近我想要的,即使我不知道我想要它!

declare

2 个答案:

答案 0 :(得分:2)

在我获得任何奖励积分之前,我不妨尝试回答第一个问题。是的,使用mapped typesinference from mapped types

可以获得您想要的内容
declare function fetchAnimalData<T>(
  animalObject: {[K in keyof T]: { getDescription(): T[K] }}
): Promise<T>;

基本上将输出类型视为T(好吧,Promise<T>),并想象一下哪种输入类型可行。它与T具有相同的键,但对于每个键K,输入属性应该是getDescription()方法,返回T[K]。让我们看看它的实际效果:

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);
}

有效!希望有所帮助!

如果我想到如何让元组工作,我会告诉你。

更新0

好吧,我想不出一个输入元组疯狂的好方法,主要是因为它涉及一个动物的单元素元组(“单个”?)或两个元素元组的联合(“我想”是动物和变形函数的“对”,其中变换函数的类型取决于动物描述的类型,这需要存在类型(根据要求提供更多信息)和TypeScript推理的类型不会在这里工作。

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

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

你传递了两个参数:动物和变形函数,它会吐出一个已被转化的新的伪动物。您可以像这样使用旧的fetchAnimalData()函数:

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如何抓取错误,而dogTailSpeed不属于Dog description的属性。这是因为artificalTuple()函数要求transformer函数参数根据getDescription()参数对animal的结果起作用。将description.dogTailSpeed更改为description.tailWagSpeed即可。

好的,我知道这不是正好你要求的东西,但它的工作原理同样适用于TypeScript。不确定我是获得奖励积分还是参加奖励。干杯!

更新1

@MichaelZlatkovsky说:

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

啊,但这不是正确的类型,如果我理解你想要的返回类型。 T的属性值应该是变换器函数的输出类型,对吧?因此,元组的第二个元素需要看起来像(input: ???) => T[K],而不是(input: T[K]) => ???

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

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

如果您将???替换为any,这将有用,但您将失去变压器功能应对动物getDescription()返回的类型起作用的重要限制方法。这就是为什么我引入artificialTuple(),它使用泛型V来替换???并保证getDescription()方法输出和变换器函数输入是兼容的(并且上面记录了错误)

您尝试向TypeScript描述的类型是这样的:

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

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

你不能比我的建议做得更好(除了将artificialTuple()的名称更改为transformAnimal()之类的东西)。您希望告诉TypeScript您不关心VV[K],只要某些类型有效,并且您不想指定它。这被称为existential type,很少有语言支持它。

好的,祝你好运!

答案 1 :(得分:1)

我想我理解你的一般想法,如果你同意稍微调整一下你的要求,我认为这是可行的。

首先,我没有看到您希望将DogZebra的实例传递给fetchAnimalData的原因 - 毕竟,fetch是应该获取这些实例并返回它们,而不是将它们作为参数接收。

其次,我不认为使用元组对请求进行编码是最好的想法 - 如何读取代码的人应该知道每个元组元素的含义?

在TypeScript中执行这些任务的一种可能方法是将请求编码为适当类型的对象,其中包含足够的数据来实际执行请求。在下面的代码中,它是AnimalRequest接口。然后,您可以拥有这些请求对象的映射,并且从fetchAndTransform参数类型获取结果对象映射的类型相对容易 - 在AnimalResultMap参数下面的代码中{ {1}}输入。

请注意,在元组出现任何问题之前,代码中的第一个问题是rany中有(r) => 5类型,因此变换器没有进行类型检查。确保为r推断出正确类型的一种方法是通过调用显式请求修饰符来表示可选转换 -  withTransform在下面的代码中。

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

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

interface AnimalRequest<D, Result = D> {
    id: number;
    descriptionType: D[]; // use empty arrays just to encode the types
    resultType: Result[];
    transform?: (animal: D) => Result; 
    withTransform<R>(transform: (animal: D) => R): AnimalRequest<D, R>;
}

type AnimalClass<D> = { new(id: number): { getDescription(): D } }

// encode the request
function request<D>(c: AnimalClass<D>, id: number): AnimalRequest<D> {
    return {
        id,
        descriptionType: [] as D[],
        resultType: [] as D[],
        withTransform<R>(transform: (animal: D) => R): AnimalRequest<D, R> {
            return Object.assign({}, this, {
                resultType: [] as R[],
                transform
            });
        }
    }
}

type AnimalRequestMap = {[n in string]: AnimalRequest<{}>}
type AnimalResultMap<RM extends AnimalRequestMap> =
    {[n in keyof RM]: RM[n]['resultType'][0]}


declare function fetchAnimalData<RM extends AnimalRequestMap>(rm: RM)
  : Promise<AnimalResultMap<RM>>;

async function f(): Promise<{}> {
    const result = await fetchAnimalData({
        dog: request(Dog, 3),
        zebraZeebs: request(Zebra, 134).withTransform(r => 5)
    })

    const t = result.dog.tailWagSpeed;
    const z = result.zebraZeebs; // number
    return {};
}