Способы обеспечения согласованности объявления API библиотеки Typescript с его реализацией?

#typescript #api-design

#typescript #api-дизайн

Вопрос:

Описание структуры проекта

В моей библиотеке с именем mylib у меня есть файл объявления API src /mylib.d.ts, написанный вручную. Есть причина для написания его вручную: я хочу сначала разработать API, а затем реализовать его (в то время tsc --declaration как флаг with делает обратное — он генерирует объявление API из реализации).

Содержимое src/mylib.d.ts:

 declare module "mylib" {
  export interface Animal {
    walk(): void;
  }
  export class Dog implements Animal {
    constructor(name: string);
    walk(run?: string): void;
    bow(): void;
  }

  export function randomAnimal(): Animal;
  export const version: string;
}
  

На этапе сборки пакета этот файл может быть скопирован как dist/index.d.ts (для ссылки с помощью "types":"./dist/index.d.ts" параметра in package.json ) или просто опубликован как @types/mylib .

Код реализации API находится в src / api.ts:

 import * as mylib from "mylib";
import { Cat, Mouse } from "./other-animals"
import { logger } from "./logger"

export class Dog implements mylib.Dog {
  walk(run?: string): void {
    logger(`I'm a dog and i ${run ? "run" : "walk"}.`);
  }
  bow(): void {
    logger("bow-wow!");
  }
}

export function randomAnimal(name?: string): mylib.Animal {
  if (name) console.log("you passed name param. It wasn't documented but ok");
  // logger(mylib.version); //if you decomment this line, the bundler will
  // fail with error because "mylib" module doesnt really exists yet. It's OK because in
  // library source code i reference `mylib.d.ts` only for type imports.
  // Or we can just add "paths": { "baseUrl": "src", "mylib": ["./api.ts"] } to tsconfig.json so
  // bundler will use it to resolve module.
  return new Dog();
}

export const version = "1.0.0";
export const undocumentedVar = 123;
  

Этот файл i точка входа для bundler : esbuild src/api.ts --bundle --outfile=dist/index.js --format=esm .

В результате в npm tarball будет 3 файла: dist/index.js , dist/index.d.ts и package.json

Проблема

Проблема в том, что все, что объявлено в mylib.d.ts, не зависит от его реализации. Например, мы можем удалить randomAnimal из api.ts, и проект все равно будет скомпилирован без каких-либо ошибок.

Мое текущее решение этой проблемы следующее: я добавляю эти строки в конец src / api:

 import * as api from "./api";
const test: typeof import("mylib") = api;

  

А затем я запускаю tsc с --noEmit "files": ["src/api.ts"] параметрами и .

Если между объявлением и реализацией будет несоответствие, я увижу ошибку.

Это довольно рабочее решение, но вопрос в том, есть ли какие-либо способы сделать это лучше? Например, без создания дополнительной неэкспортируемой константы?

Ответ №1:

Это довольно странный способ сделать это. API (контракт) сначала имеет смысл, но вместо записи .d.ts файлов просто напишите типы интерфейсов, а не вашу реализацию.

Я не думаю, что запись .d.ts файлов вручную, а затем реализация — это шаблон, который действительно поддерживает Typescript.

Комментарии:

1. Посмотрите на пример vscode: github.com/microsoft/vscode/blob/master/src/vs/vscode.d.ts . Этот файл определенно написан вручную. А затем оно публикуется в @types как есть.

2. Да, имеет смысл писать определения для файлов .js, которые уже существуют. То, что вы пытаетесь сделать, — это просто не рабочий процесс, который хорошо поддерживается или предназначен.

3. Не могли бы вы дать совет о том, как можно изменить текущую структуру проекта?

4. Если вы пишете typescript, просто напишите typescript. Вы можете избежать написания реализации, написав сначала интерфейсы и типы.

5. Проблема с созданием соответствующих файлов .d.ts из .ts заключается в том, что я активно использую настройку путей tsconfig, чтобы избежать импорта из «../../../../» выражения в моем коде. Пакет разрешает эти пути при генерации кода javascript, но tsc этого не делает при создании файлов .d.ts.