为了便于讨论,我将定义两种不同的对象类型和两种不同的查询端点:
interface FishToEat {
count: number,
fishType: string,
}
// endpoint is "/fishes"
interface WineToDrink {
vintner: string;
vintage: number;
}
// endpoint is "/wines"
TypeScript 中的对象类型如FishToEat 仅存在于 TypeScript 代码中;当您的代码编译为 JavaScript 时,它们完全是 erased。在运行时,将不存在FishToEat 的痕迹。所以没有希望编写像这样的 TypeScript 代码
const fish = getValue<FishToEat>();
const wine = getValue<WineToDrink>();
并让fish 和wine 在运行时成为具有不同属性的不同对象。以上将编译为 JavaScript 代码,如
const fish = getValue();
const wine = getValue();
而且我认为我们可以同意,无论您如何尝试实现 getValue(),它都不会神奇地知道要查询哪个端点。
与其尝试那样做,不如想出一些符合您需要的惯用 JavaScript 代码,然后在 TypeScript 中给它键入以帮助您使用它。由于您必须将 something 传递给 getValue() 才能选择端点,我们不妨选择最容易使用的东西。大概这可能只是端点路径:
// pass in endpoint path
getValue("/fishes");
getValue("/wines");
但如果这些端点路径暴露了您不想暴露的实现细节,您可以传入一些其他更友好的字符串,实现可以映射到端点:
// pass in friendly query name
getValue("FishToEat");
getValue("WineToDrink");
听起来你更喜欢这种方法,所以让我们来实现它并给它一些类型。
我们需要一些方法将友好的字符串映射到端点路径,所以让我们创建一个对象来保存这个映射:
const typeNameToEndpoint = {
FishToEat: "/fishes",
WineToDrink: "/wines"
} as const;
通过使用const assertion,我们要求编译器将typeNameToEndpoint 视为从键名到字符串literal types"/fishes" 和"/wines" 的不变映射。如果我们不这样做,编译器会推断出{FishToEat: string, WineToDrink: string} 的类型,这是正确的,但没有用;我们希望编译器知道它正在查询哪个端点,这样它就可以知道它应该返回哪种类型。
这是另一个单独的、纯类型级别的映射,因此我们可以将其定义为interface:
interface EndpointToTypeMap {
"/fishes": FishToEat;
"/wines": WineToDrink;
}
现在getValue() 的实现可能如下所示:
function getValue<K extends keyof typeof typeNameToEndpoint>(typeName: K) {
const endpoint = typeNameToEndpoint[typeName];
console.log(endpoint);
// fetch(endpoint) or something
return {} as Promise<EndpointToTypeMap[typeof endpoint]> // need a real impl here
}
类型参数K中的generic对应typeNameToEndpoint对象的keys,作为typeName传入。该实现在该对象中查找typeName 并获得endpoint,编译器已对其进行强类型化以依赖于通用K。您可以运行查询,然后返回一个正确类型的值,即EndpointToTypeMap[typeof endpoint],并且还依赖于K。
哦,既然你显然在调用一个 rest api,我们想要一个 Promise 我猜。让我们试试吧:
const fish = await getValue("FishToEat") // logs "/fishes"
// const fish: FishToEat
const wine = await getValue("WineToDrink") // logs "/wines"
// const wine: WineToDrink
太好了。编译器知道fish 是FishToEat,而wine 是WineToDrink。
这是基本方法。您可能不满意,接口名称FishToEat 和WineToDrink 与作为typeName 参数"FishToEat" 和"WineToDrink" 传递的值之间存在冗余。这在技术上并不是多余的,因为 TypeScript 的类型系统在运行前被完全擦除,并且无论如何都不是 nominal type system,所以事实上你在 TypeScript 代码中写出了字符 ⒻⓘⓢⓗⓉⓞⒺⓐⓣ因为类型的名称和字符串的值更多的是巧合而不是冗余。
不过,也许您想尝试消除其中之一。好吧,你不能消除运行时字符串。你能做的最好的就是消除类型名称:
interface EndpointToTypeMap {
"/fishes": {
count: number,
fishType: string,
};
"/wines": {
vintner: string;
vintage: number;
};
}
现在没有名为 FishToEat 或 WineToDrink 的类型,当您调用 getValue() 时,您将返回相同强对象类型的值,但它们将是匿名的:
const fish = await getValue("FishToEat") // logs "/fishes"
/* const fish: {
count: number;
fishType: string;
} */
const wine = await getValue("WineToDrink") // logs "/wines"
/* const wine: {
vintner: string;
vintage: number;
} */
这样更好吗?我对此表示怀疑;在这里,用户友好可能比 DRY 更好。
Playground link to code