next.js 我如何编写一个函数来查找具有类型安全参数的URL?

z5btuh9x  于 2023-11-18  发布在  其他
关注(0)|答案(2)|浏览(106)

我有一个NextJS/React应用程序。我们维护了一个内部url的大对象,每个url都定义了一个createPath函数,看起来像这样:

const URLS = {
    dashboard: {
        createPath: ({ accountNumber }: { accountNumber: string }) => `/account/${accountNumber}`
    },
    property: {
        createPath: ({ accountNumber, propertyId }: { accountNumber: string, propertyId: string }) => `/account/${accountNumber}/property/${propertyId}`
    }
}

字符串
所以当我们在页面之间进行链接时,我们会做这样的事情:

<Link
  href={URLS.DASHBOARD.createPath({
  accountNumber: 'ABC123', // usually account number come out of the route params
 })}
>
 Go to account
</Link>


这很好用,但这意味着整个项目都在使用该URL的结构。实际上,对象中有很多URL,还有一些嵌套,这使得在没有大量查找和替换的情况下很难更改。
受django reverse工具的启发,我希望能够写这样的东西:

getUrl('dashboard', { 'accountNumber': 'ABC123' })


这并不难做到,如果写这样的东西:

export const getUrl = (routeName: RouteNames, params: any): string =>
    URLS[routeName].createPath(params);


但是,如果我想在参数上使用类型安全呢?
我曾经尝试过(TypescriptPlayground)使用重载来做这件事,所以

type RouteNames = 'property' | 'dashboard';

const URLS = {
    dashboard: {
        createPath: ({ accountNumber }: { accountNumber: string }) => `/account/${accountNumber}`
    },
    property: {
        createPath: ({ accountNumber, propertyId }: { accountNumber: string, propertyId: string }) => `/account/${accountNumber}/property/${propertyId}`
    }
}

function getUrl(
    routeName: 'dashboard',
    params: {
        accountNumber: string;
    }
): string;

function getUrl(
    routeName: 'property',
    params: {
        accountNumber: string;
        propertyId: string;
    }
): string;

function getUrl(
    routeName: RouteNames,
    params: any
): string {
    return URLS[routeName].createPath(params);
}

console.log(getUrl('dashboard', {
    accountNumber: 'ABC123',
}))
// prints  "/account/ABC123" 
console.log(getUrl('property', {
    accountNumber: 'ABC123',
    propertyId: 'DEF456',
}))
// prints "/account/ABC123/property/DEF456" 

console.log(getUrl('property', {
    accountNumber: 'ABC123',
    propertyID: 'DEF456',
}))

// correct TS error: Object literal may only specify known properties, but 'propertyID' does not exist in type '{ accountNumber: string; propertyId: string; }'. Did you mean to write 'propertyId'?
// prints "/account/ABC123/property/undefined"


这工作得很好,但这意味着当我创建一个新的url时,我有三个不同的结构要添加-RouteNamesURLS * 和 * getUrl。有人有什么建议吗?我已经仔细检查了NextJS docs,看看我是否只是缺少一个库实用程序,无法找到一个,我也尝试过在getUrl签名中为参数使用联合类型(所有不同的参数集),但是每个createPath也必须使用联合,安全性被大大削弱。

dvtswwa3

dvtswwa31#

这种方法可能很有前途。

const URLS = {
    dashboard: {
        createPath: ({ accountNumber }: { accountNumber: string }) => `/account/${accountNumber}`
    },
    property: {
        createPath: ({ accountNumber, propertyId }: { accountNumber: string, propertyId: string }) => `/account/${accountNumber}/property/${propertyId}`
    }
} as const;

function getUrl<K extends keyof typeof URLS>(key:K, ...params: Parameters<typeof URLS[K]["createPath"]>){
    const createPath = URLS[key].createPath as (...args:unknown[]) => string;
    return createPath(...params)
}

console.log(getUrl('dashboard', {
    accountNumber: 'ABC123',
}))
// prints  "/account/ABC123" 
console.log(getUrl('property', {
    accountNumber: 'ABC123',
    propertyId: 'DEF456',
}))

// excess property
console.log(getUrl('dashboard', {
    accountNumber: 'ABC123',
    propertyId: 'DEF456',
}))

// misnamed property
console.log(getUrl('property', {
    accountNumber: 'ABC123',
    propertyID: 'DEF456',
}))

// correct TS error: Object literal may only specify known properties, but 'propertyID' does not exist in type '{ accountNumber: string; propertyId: string; }'. Did you mean to write 'propertyId'?
// prints "/account/ABC123/property/undefined"

字符串

pdtvr36n

pdtvr36n2#

你可以使用一个constAssert(as const),这个Assert被一个类型所约束,这个类型代表了你期望的URLS对象的模式(使用satisfies运算符),来指示编译器使用文字推断来推断对象的类型。(这也提供了防止在你的程序中稍后发生变化的好处。)
然后,您可以派生一个类型RouteName,它表示URLS对象的键,以及一个类型实用程序RouteParams,它将index关联路由的createPath函数的参数类型(使用Parameters<Type>实用程序)。
这些类型可以用来创建一个通用的getUrl函数,这将帮助你达到你的目标:以一种方式派生类型,通过只需要修改初始数据结构来使未来的更改更加容易。下面是一个例子:
TSPlayground

const URLS = {
  dashboard: {
    createPath: ({ accountNumber }: { accountNumber: string }) =>
      `/account/${accountNumber}`,
  },
  property: {
    createPath: (
      { accountNumber, propertyId }: {
        accountNumber: string;
        propertyId: string;
      },
    ) => `/account/${accountNumber}/property/${propertyId}`,
  },
} as const satisfies Record<string, { createPath: (value: any) => string }>;

type RouteName = keyof typeof URLS;

type RouteParams<R extends RouteName> = Parameters<
  typeof URLS[R]["createPath"]
>[0];

function getUrl<R extends RouteName>(
  routeName: R,
  params: RouteParams<R>,
): string {
  // @ts-expect-error
  return URLS[routeName].createPath(params);
}

console.log(getUrl("dashboard", {
  accountNumber: "ABC123",
}));

console.log(getUrl("property", {
  accountNumber: "ABC123",
  propertyId: "DEF456",
}));

console.log(getUrl("property", {
  accountNumber: "ABC123",
  propertyID: "DEF456", /* Error
  ~~~~~~~~~~
  Argument of type '{ accountNumber: string; propertyID: string; }' is not assignable to parameter of type '{ accountNumber: string; propertyId: string; }'.
  Object literal may only specify known properties, but 'propertyID' does not exist in type '{ accountNumber: string; propertyId: string; }'. Did you mean to write 'propertyId'? (2345) */
}));

字符串

相关问题