Skip to content

Proposal: JavaScript/TypeScript configuration files (tsp.config.js / tsp.config.ts) for explicit library resolution #9057

@Hajime-san

Description

@Hajime-san

Clear and concise description of the problem

Intro

Currently, TypeSpec primarily relies on tspconfig.yaml for configuration. This YAML-based configuration requires TypeSpec to resolve custom linter libraries and dependencies based on string names internally.

This internal resolution process implicitly depends on the existence of a traditional node_modules structure. This creates significant friction and requires workarounds in modern JavaScript runtimes like Deno, which do not create node_modules directories by default.

For example, to use a custom linter in a Deno environment, developers must currently create an explicit workaround involving symbolic links and dummy package.json files within an emulated node_modules structure to satisfy TypeSpec's internal library resolution logic.

Therefore, I propose adding support for JavaScript (tsp.config.js) and TypeScript (tsp.config.ts) configuration files.

This approach, similar to the modern "Flat Config" adopted by ESLint, allows the configuration to be defined as a JavaScript object, enabling the use of standard ESM import statements.

By using import, we delegate library and module resolution to the host JavaScript runtime. This eliminates TypeSpec's need to parse strings and implicitly rely on file system conventions like node_modules.

Example

// tsp.config.ts
import { defineConfig } from "@typespec/compiler";
import bestPractices from "@typespec/best-practices/recommended"; 
import myCoolLinterRule from "./linter.ts";
import openapi3Emitter from "@typespec/openapi3";

export default defineConfig({
  emit: [
    openapi3Emitter,
  ],

  options: {
    "@typespec/openapi3": {
      "emitter-output-dir": "{output-dir}",
      "output-file": "{service-name}.{version}.spec.yaml",
    },
  },

  linter: {
    extends: [
      bestPractices,
      myCoolLinterRule
    ],
  },
});

// linter.ts
import { defineLinter } from "@typespec/compiler";
import { requiredDocRule } from "./rules/required-doc.rule.ts";

export const $linter = defineLinter({
  ...
});

in YAML:

emit:
  - "@typespec/openapi3"

options:
  "@typespec/openapi3":
    emitter-output-dir: "{output-dir}"
    output-file: "{service-name}.{version}.spec.yaml"

linter:
  library:
    - "@typespec/best-practices/recommended"

Benefits and Rationale

True Runtime Agnosticism: By delegating resolution to the host runtime (via import), TypeSpec achieves better compatibility with diverse environments without requiring file system hacks.

Type Safety for TypeScript: Importing a defineConfig helper allows users to write their configuration with full type safety, IntelliSense, and compilation checks. This is especially useful now that runtimes like Node.js 24+ can execute TypeScript files directly.

Simplified Parser: This change reduces the complexity within TypeSpec's compiler, as the need for a custom YAML parser and string-based module resolution logic is diminished or removed for the JS/TS config path.

Dynamic Configuration: Users gain the ability to use standard JavaScript logic (e.g., conditional statements, environment variables) to dynamically generate their configurations.

Checklist

  • Follow our Code of Conduct
  • Read the docs.
  • Check that there isn't already an issue that request the same feature to avoid creating a duplicate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    compiler:coreIssues for @typespec/compilerdesign:neededA design request has been raised that needs a proposaltriaged:core

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions