#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.