Typescript:对象的typesafety必须具有数组值中的所有键

Typescript:对象的typesafety必须具有数组值中的所有键,typescript,Typescript,在Typescript ^3.8中,给定此接口 接口IEndpoint{method:'get'|'put'|'post'|'patch'|'delete',path:string} 这个常数 const endpoint={method'get',path:'/first/:firstId/second/:secondId'} 请注意:firstId和:secondId是将在运行时动态提供的路径参数。我有一个函数,它将获取端点和带有param值的对象,并返回url 函数buildEndpoin

在Typescript ^3.8中,给定此接口

接口IEndpoint{method:'get'|'put'|'post'|'patch'|'delete',path:string} 这个常数

const endpoint={method'get',path:'/first/:firstId/second/:secondId'} 请注意:firstId和:secondId是将在运行时动态提供的路径参数。我有一个函数,它将获取端点和带有param值的对象,并返回url

函数buildEndpointUrlendpoint:IEndpoint,映射:{[key:string]:string}:string; 例如:

//将url设置为“/first/123/second/456” constURL=buildEndpointUrlendpoint,{firstId:'123',secondId:'456'}; 我面临的挑战是编译器将允许垃圾作为第二个参数传递:如何定义IEndpoint和buildEndpointUrl,以便在作为第二个参数提供的对象缺少必需的键时编译器抛出错误

以下是我尝试过的:

接口IEndpoint{ 方法:“获取”|“放置”|“发布”|“补丁”|“删除”, 路径:字符串 } 常量端点:IEndpoint={…}; 函数buildEndpointUrl 端点:IEndpointConfig, 映射:{[key:T[number]]:string}//编译器错误 ; 最后一行抛出编译器错误:

TS1023:索引签名参数必须是字符串或数字

我希望T[number]与string等价,因为T扩展了ReadonlyArray,但显然不是。我应该如何设置我的定义以添加类型安全性?

您几乎做到了:

type EndpointParams = ReadonlyArray<string>;

interface IEndpoint<T extends EndpointParams> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: string
}

function buildEndpointUrl<T extends EndpointParams>(
  endpoint: IEndpoint<T>, 
  map: {[key in T[number]]: string} // In your case it should be mapped, not just indexed
) {}

const endpoint: IEndpoint<['first', 'second']> = {
    method: "get",
    path: "",
};

buildEndpointUrl(endpoint, { // failed
    first: "v1",
    p2: "v2",
});

buildEndpointUrl(endpoint, { // passed
    first: "v1",
    second: "v2",
});
你几乎成功了:

type EndpointParams = ReadonlyArray<string>;

interface IEndpoint<T extends EndpointParams> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: string
}

function buildEndpointUrl<T extends EndpointParams>(
  endpoint: IEndpoint<T>, 
  map: {[key in T[number]]: string} // In your case it should be mapped, not just indexed
) {}

const endpoint: IEndpoint<['first', 'second']> = {
    method: "get",
    path: "",
};

buildEndpointUrl(endpoint, { // failed
    first: "v1",
    p2: "v2",
});

buildEndpointUrl(endpoint, { // passed
    first: "v1",
    second: "v2",
});

您只需要一个映射类型而不是索引签名。预定义的映射类型记录将起作用

export interface IEndpoint<T extends ReadonlyArray<string>> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: string
}

const endpoint: IEndpoint<['firstId', 'secondId']> =  { method: 'get', path: '/first/:firstId/second/:secondId' };

declare function buildEndpointUrl<T extends ReadonlyArray<string>>(
  endpoint: IEndpoint<T>, 
  map: Record<T[number],string> // compiler error
): void;

const b = buildEndpointUrl(endpoint, { firstId: "", secondId:"", test: "" })

注意:在4.1中,您还可以使用从路径字符串中实际提取参数

export interface IEndpoint<T extends string> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: T
}

type ExtractParameters<T extends string> = 
  T extends `${infer Prefix}/:${infer Param}/${infer Suffix}` ? Record<Param, string> & ExtractParameters<Suffix> & [Prefix, Suffix, Param] :
  T extends `${infer Prefix}/:${infer Param}` ? Record<Param, string>  :
  T extends `:${infer Param}`? Record<Param, string> :
  { T: T}

type X = "second/:secondId" extends `${infer Prefix}/:${infer Param}/${infer Suffix}` ? [Prefix, Param, Suffix] : "";
type Y = ExtractParameters<"/first/:firstId/second/:secondId">

const endpoint =  { method: 'get', path: '/first/:firstId/second/:secondId' } as const

declare function buildEndpointUrl<T extends string>(
  endpoint: IEndpoint<T>, 
  map: ExtractParameters<T>
): void;

const b = buildEndpointUrl(endpoint, { firstId: "", secondId:"", test: "" })

您只需要一个映射类型而不是索引签名。预定义的映射类型记录将起作用

export interface IEndpoint<T extends ReadonlyArray<string>> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: string
}

const endpoint: IEndpoint<['firstId', 'secondId']> =  { method: 'get', path: '/first/:firstId/second/:secondId' };

declare function buildEndpointUrl<T extends ReadonlyArray<string>>(
  endpoint: IEndpoint<T>, 
  map: Record<T[number],string> // compiler error
): void;

const b = buildEndpointUrl(endpoint, { firstId: "", secondId:"", test: "" })

注意:在4.1中,您还可以使用从路径字符串中实际提取参数

export interface IEndpoint<T extends string> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: T
}

type ExtractParameters<T extends string> = 
  T extends `${infer Prefix}/:${infer Param}/${infer Suffix}` ? Record<Param, string> & ExtractParameters<Suffix> & [Prefix, Suffix, Param] :
  T extends `${infer Prefix}/:${infer Param}` ? Record<Param, string>  :
  T extends `:${infer Param}`? Record<Param, string> :
  { T: T}

type X = "second/:secondId" extends `${infer Prefix}/:${infer Param}/${infer Suffix}` ? [Prefix, Param, Suffix] : "";
type Y = ExtractParameters<"/first/:firstId/second/:secondId">

const endpoint =  { method: 'get', path: '/first/:firstId/second/:secondId' } as const

declare function buildEndpointUrl<T extends string>(
  endpoint: IEndpoint<T>, 
  map: ExtractParameters<T>
): void;

const b = buildEndpointUrl(endpoint, { firstId: "", secondId:"", test: "" })

至少[key:T[number]]:string应该是[key:number]:string我认为这行不通。它不会强制参数具有所需的属性。在上面的示例中,如果我提供IEndpoint作为第一个函数参数,我希望Typescript推断第二个参数的类型必须是{firstId:string,secondId:string}如果调用看起来像buildEndpointUrlendpoint,['firstId',secondId'];?不第二个参数是用于将路由参数“firstId”的名称映射到其值“123”的对象。我在我的帖子中举了一个例子。我觉得这个问题和答案对你会很有用:至少[key:T[number]]:string应该是[key:number]:string我认为这行不通。它不会强制参数具有所需的属性。在上面的示例中,如果我提供IEndpoint作为第一个函数参数,我希望Typescript推断第二个参数的类型必须是{firstId:string,secondId:string}如果调用看起来像buildEndpointUrlendpoint,['firstId',secondId'];?不第二个参数是用于将路由参数“firstId”的名称映射到其值“123”的对象。我在我的帖子中举了一个例子。我觉得这个问题和答案对你很有用: