【问题标题】:Typescript: typesafety for object must have all keys in array values打字稿:对象的类型安全必须具有数组值中的所有键
【发布时间】:2020-11-17 05:38:12
【问题描述】:

在 Typescript ^3.8 中,给定这个接口...

interface IEndpoint { method: 'get'|'put'|'post'|'patch'|'delete', path: string }

还有这个常数...

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

请注意,:firstId:secondId 是在运行时动态提供的路径参数。我有一个函数,它将获取端点和一个带有参数值的对象,并返回 url。

function buildEndpointUrl(endpoint: IEndpoint, map: {[key: string]: string}): string;

例如:

// will set url to '/first/123/second/456'
const url = buildEndpointUrl(endpoint, {firstId: '123', secondId: '456'});

我面临的挑战是编译器将允许将垃圾作为第二个参数传递:我如何定义IEndpointbuildEndpointUrl,以便如果对象作为第二个参数提供,编译器会抛出错误缺少必需的密钥?

这是我尝试过的:

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

const endpoint: IEndpoint<['firstId', 'secondId']> = {...};

function buildEndpointUrl<T extends ReadonlyArray<string>>(
  endpoint: IEndpointConfig<T>, 
  map: {[key: T[number]]: string} // compiler error
);

最后一行抛出编译器错误:

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

我希望 T[number] 等同于 string,因为 T extends ReadonlyArray&lt;string&gt; 但显然不是。我应该如何设置我的定义来增加类型安全?

【问题讨论】:

  • 至少[key: T[number]]: string 应该是[key: number]: string
  • 我认为这行不通。它不会强制参数具有所需的属性。在上面的示例中,如果我提供 IEndpoint&lt;['firstId', 'secondId']&gt; 作为第一个函数参数,我希望 Typescript 推断第二个参数的类型必须是 {firstId: string, secondId: string}
  • 呼叫应该类似于buildEndpointUrl(endpoint, ['firstId', 'secondId']);
  • 否;第二个参数是一个对象,用于将路由参数的名称('firstId')映射到它的值('123')。我在帖子中举了一个例子。
  • 我觉得这个问题和答案会对你有用:stackoverflow.com/questions/64744734/…

标签: typescript


【解决方案1】:

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

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: "" })

Playground Link

注意在 4.1 中你也可以使用template literal types 来实际提取路径字符串中的参数

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: "" })

Playground Link

【讨论】:

    【解决方案2】:

    你几乎明白了:

    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",
    });
    

    【讨论】:

      猜你喜欢
      • 2020-08-12
      • 2018-05-30
      • 1970-01-01
      • 1970-01-01
      • 2018-12-16
      • 2020-04-19
      • 2020-05-09
      • 1970-01-01
      • 2020-04-28
      相关资源
      最近更新 更多