Typescript :: 在一个对象中,使用键字符串来计算它的值类型

Typescript :: in an object, use key string to compute its value type

我发现了这个很棒的 fastify 插件 (https://github.com/Coobaha/typed-fastify),它使用以下语法推断传递给服务的正确类型,给定类型 T 的模式。

import {Schema, Service} from '@coobaha/typed-fastify';


interface ExampleSchema extends Schema {
  paths: {
    'GET /test/:testId': {
      request: {
        params: {
          testId: {
            type: 'string',
          }
        };
      };
      // other properties, not relevant for question
    };
  };
}

const exampleService: Service<ExampleSchema> = {
  'GET /test/:testId': (req, reply) => {
    const testId = req.params.testId // this passes ✅
    const testName = req.params.testName // this fails  
 
    // rest of handler
  }
};

// example is simplified. Find a full example here: https://github.com/Coobaha/typed-fastify

这个插件已经很棒了,但我想更进一步。

给定一个 /test/:testId 字符串,我们 已经知道 URL 需要哪些参数。必须指定它两次(在 URL params 对象中),在我看来,这似乎值得尝试自动化以保证它们始终留在同步。

我很想找到一种方法来自动添加 params 类型,从键字符串(端点 URL)计算它。例如,键 /test/:testId 应该有一个 params 类型 {testId: string}.

想象一下:

import {Schema, Service} from '@coobaha/typed-fastify';


interface ExampleSchema extends Schema {
  paths: {
    'GET /test/:testId': {
       // only other properties, no need for explicit params
    };
  };
}

// and still have the same type-checks on the params :
const exampleService: Service<ExampleSchema> = {
  'GET /test/:testId': (req, reply) => {
    const testId = req.params.testId // this passes ✅
    const testName = req.params.testName // this fails  
 
    // rest of handler
  }
};

据我所知,这对于默认的 typescript 功能是不可能的,并且可能只能在编译时使用 ttypescript 使用插件。

有什么方法可以避免使用替代编译器和编写自己的插件吗?

感谢您的支持

您可以通过 template literal types introduced in TypeScript 4.1, though for long paths with many parameters, you'll need TypeScript 4.5 for its detection of tail recursive types: Playground

实现
type ExtractParams<T extends string, Acc = {}> =
  T extends `${infer _}:${infer P}/${infer R}` ? ExtractParams<R, Acc & { [_ in P]: string }> :
  T extends `${infer _}:${infer P}` ? Acc & { [_ in P]: string } :
  Acc

type Service<Schema extends BaseSchema> = {
  [K in keyof Schema['paths'] & string]: (req: Req<ExtractParams<K>>, reply: Reply) => void;
}

const exampleService: Service<ExampleSchema> = {
  'GET /test/:testId': (req, reply) => {
    const testId = req.params.testId // this passes ✅
    const testName = req.params.testName // this fails ✅
  }
};

魔法在ExtractParams类型。这是通过尝试两种模式来实现的。首先,它检查 *:(param)/(rest)。如果匹配,它将推断的 param 添加到累加器并递归 rest。这会在查询中间捕获参数(例如 GET /test/:HERE/later)。然后,它检查 *:(param) 以检测末尾的参数(例如 GET /test/:testId)。如果这些都不匹配,则处理完成并且它 returns 具有任何推断参数的累加器。