如何在 TypeScript 中推断函数类型的类型参数定义?



我正在尝试扩展以下Next.js函数类型:

export type GetStaticProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery,
D extends PreviewData = PreviewData
> = (
context: GetStaticPropsContext<Q, D>
) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P>

这样它的上下文(GetStaticPropsContext)上的3个属性永远不会undefined,像这样:

type UpdateContext<T> = T extends (context: infer Context extends GetStaticPropsContext) => infer Return
? (context: SelectiveRequiredNotUndefined<Context, "locale" | "locales" | "defaultLocale">) => Return
: never;

现在,如果我这样做,我可以使用永远不会undefined上下文属性的新类型:

type MyGetStaticProps = UpdateContext<GetStaticProps>;
const getStaticProps: MyGetStaticProps = async (context) => { 
// do stuff
}

问题是MyGetStaticProps不再是通用的。我知道我总是可以这样做:

type MyProps = {
someField: string
};
export const getStaticProps: UpdateContext<GetStaticProps<MyProps>> = async (context) => {
// do stuff
}

但我想知道是否有办法推断类型参数定义以避免更冗长的语法?

回顾一下,我正在寻找一种拥有某种type MyGetStaticProps = UpdateContext<GetStaticProps>;的方法,但这将支持MyGetStaticProps<MyProps>(推断GetStaticProps的类型参数)

这是示例的完全工作的游乐场链接

不幸的是,TypeScript 目前无法表示这些类型的类型(称为"高级类型"),请参阅 microsoft/TypeScript#1213。

您可以尝试实施它们,就像这篇博文或此评论显示的那样,但它很笨拙,不能完全代表HKT可以做什么。

最好使用替代方案,或者尝试重新考虑您的问题,甚至首先通过重新设计代码的某些部分来防止它。

简而言之,你不能真正获取一个类型,修改它,然后吐出类型,就像你可以使用 JS 函数一样:

// type GetStaticProps<...> = ...;
function GetStaticProps(...) { ... }
// type UpdateContext<Func ...> = ...;
function UpdateContext(Func) {
const NewFunc = (...args) => {
args[0].prop = "something else";
return Func(...args);
};
return NewFunc;
}
// type Updated = UpdateContext<GetStaticProps>;
const Updated = UpdateContext(GetStaticProps);
// type Result = Updated<...>; // but Updated can't be a generic, it's a result of another type.
const Result = Updated({ prop: "will change" });

jcalz的一些HKT评论(更深入地为您提供潜在的解决方法):

  • TypeScript 中的高阶类型函数?

  • 在打字稿的泛型中使用泛型

  • 如何在 TypeScript 中使用泛型参数扩展泛型参数?

问题

我花了一段时间才弄清楚你在这里想要实现的目标,但我想我终于明白你的目标是:

  1. 优化GetStaticPropsContext,以便context的某些属性是必需的且不可为空。
  2. 能够从函数中infer返回的 props 的类型,而不是自己定义它们。

单独解析 #2 (没有 #1)很简单,因为 Next.js为此导出InferGetStaticPropsType<T>实用程序类型。

export type InferGetStaticPropsType<T> = T extends GetStaticProps<infer P, any>
? P
: T extends (
context?: GetStaticPropsContext<any>
) => Promise<GetStaticPropsResult<infer P>>
? P
: never

问题的核心是实现 #1 将使此类型不再有效。 这是因为接受更广泛参数的函数extends接受这些参数的较窄细化的函数,但反之则不然。

该原则的任意说明:

// this is `true`
type A = ((arg: number | string) => any) extends ((arg: string) => any) ? true : false;
// this is `false`
type B = ((arg: number) => any) extends ((arg: number | string) => any) ? true : false;

如果您的getStaticProps函数只能接受精细的context类型,则InferGetStaticPropsType<T>实用程序类型将不再起作用,因为您的getStaticProps不再extends基本GetStaticProps

export const getStaticPropsA = async (context: GetStaticPropsContext) => {
return {
props: {
someField: 'yes'
}
}
};
// this is `{ someField: string; }`
type PropsA = InferGetStaticPropsType<typeof getStaticPropsA>

export const getStaticPropsB = async (context: SelectiveRequiredNotUndefined<GetStaticPropsContext, "locale" | "locales" | "defaultLocale">) => {
return {
props: {
someField: 'yes'
}
}
};
// this is `never`
type PropsB = InferGetStaticPropsType<typeof getStaticPropsB>

溶液

由于要推断返回类型,因此将像GetStaticProps这样的类型分配给整个函数是没有意义的。 您只需为参数分配一个类型。 您已经完成了SelectiveRequiredNotUndefined实用程序类型的该部分。

缺少的是推断返回类型的能力。 为此,我们需要修改一些核心的 Next.js 类型。

方法1:模块增强

localelocalesdefaultLocale属性标记为可选,因为并非所有应用都将使用国际化。 但是,如果你的应用在 next.config.js 文件中包含此路由信息,则可以安全地假定这些属性将始终存在。

您可以使用 TypeScript 模块扩充来修改nextGetStaticPropsContext类型的类型。

import { PreviewData } from 'next';
import { ParsedUrlQuery } from 'querystring';
declare module 'next' {
export type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
params?: Q
preview?: boolean
previewData?: PreviewData
locale: string
locales: string[]
defaultLocale: string
}
}

应该是简单的解决方案,但有时正确设置可能会很痛苦。 我认为我们不必也覆盖使用它的其他类型,例如GetStaticPropsInferGetStaticPropsType,但也许我们会这样做?

这个游乐场有一些错误,但它确实得到了正确的类型。

方法 2:本地类型

如果扩充模块太麻烦而无法工作,您可以继续您已经在尝试的操作,即创建您自己的 Next.js 类型的修改副本。 您将从我们自己的本地类型文件导入,而不是导入 Next.js 版本。

import { GetStaticPropsContext, GetStaticPropsResult } from 'next';
import { ParsedUrlQuery } from 'querystring';
export type MyGetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> =
GetStaticPropsContext<Q> & Required<Pick<GetStaticPropsContext, 'locale' | 'locales' | 'defaultLocale'>>;
export type MyGetStaticProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery
> = (context: MyGetStaticPropsContext<Q>) => Promise<GetStaticPropsResult<P>>
export type MyInferGetStaticPropsType<T> = T extends MyGetStaticProps<infer P, any>
? P
: T extends (
context?: MyGetStaticPropsContext<any>
) => Promise<GetStaticPropsResult<infer P>>
? P
: never
// ==== Example use:
export const getStaticProps = async (context: MyGetStaticPropsContext) => {
context.locale
//       ^? (property) locale: string
context.locales
//       ^? (property) locales: string[]
context.defaultLocale
//       ^? (property) defaultLocale: string
context.preview // As an example of a property that doesn't get modified
//       ^? (property) preview?: boolean | undefined
return {
props: {
someField: 'yes'
}
}
};
type MyProps = MyInferGetStaticPropsType<typeof getStaticProps>
//   ^? type MyProps = { someField: string; }

打字稿游乐场链接

最新更新