Typescript,基于模板化函数的类型创建字符串



我的代码类似于下面:

// this calls a rest api endpoint
function getValue<T>(endPoint: string): T {}
// example of use
const myvalue = getValue<FishToEat>("/fishes");

我希望能够删除端点参数,并从以下内容获得此字符串:

interface TypeToEndpointMap {
FishToEat: "/fishes"
}

上面的界面显然是垃圾,但这是可能的,我怎么能做到这一点?

这是一个可以运行的版本,但不会像希望的那样工作:

interface FishToEat {
count: number,
fishType: string,
}
let TypeToEndpointMap: { [key: string]: string } = {};
TypeToEndpointMap.FishToEat = "/fishes";

// this calls a rest api
function getValue<T>(pathName: string): T {
const endpoint = typeof getValue;
console.log(endpoint);
return {} as T;
}
const myvalue = getValue<FishToEat>("/fishes");

尝试jcalz版本,我无法显示最终结果,以澄清其从类型到端点映射我需要。该类型位于一个泛型函数中,我们将需要在该泛型函数中使用强值。编辑你的版本,以更多的我需要,但不能让它工作,在这里

为了便于讨论,我将定义两个不同的对象类型和两个不同的查询端点:

interface FishToEat {
count: number,
fishType: string,
}
// endpoint is "/fishes"
interface WineToDrink {
vintner: string;
vintage: number;
}
// endpoint is "/wines"

FishToEat这样的TypeScript对象类型只存在于TypeScript代码中;当您的代码编译为JavaScript时,它们将被完全删除。在运行时,不存在FishToEat的踪迹。所以没法写出像

这样的TypeScript代码
const fish = getValue<FishToEat>(); 
const wine = getValue<WineToDrink>();

并使fishwine在运行时成为具有不同属性的有用的不同对象。上面的代码将编译成像

这样的JavaScript代码
const fish = getValue();
const wine = getValue();

,我认为我们可以同意,无论你如何尝试实现getValue(),它都不会神奇地知道要查询哪个端点。


与其尝试那样做,不如想出一些习惯的JavaScript代码,让它按照你想要的方式运行,然后在TypeScript中给它类型来帮助你使用它。由于您必须将内容传递给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断言,我们要求编译器将typeNameToEndpoint视为从键名到字符串字面值类型"/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中是泛型的,对应于typeNameToEndpoint对象的键,作为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

好了。编译器知道fishFishToEat,wineWineToDrink


这是基本方法。您可能不满意的是,接口名称FishToEatWineToDrink与作为typeName参数"FishToEat""WineToDrink"传入的值之间存在冗余。这在技术上并不是多余的,因为TypeScript的类型系统在运行前被完全擦除,并且无论如何都不是一个名义上的类型系统,所以你在TypeScript代码中写出字符ⒻⓘⓢⒺ作为类型的名称和字符串的值的事实更多的是巧合而不是冗余。

不过,也许你想尝试消除其中一个。你不能消除运行时字符串。你能做的最好的事情就是消除类型名:

interface EndpointToTypeMap {
"/fishes": {
count: number,
fishType: string,
};
"/wines": {
vintner: string;
vintage: number;
};
}

现在没有名为FishToEatWineToDrink的类型,当你调用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链接到代码

这似乎是不可能的,无法从其中获得泛型函数的类型。在这里无法获取类型字符串映射到获取端点。

相关内容

  • 没有找到相关文章

最新更新