Skip to content

Oxlint 타입 인지 프리뷰

이 글은 타입 인지 린팅의 기술 프리뷰를 알립니다. 안정성·설정·규칙 커버리지가 개선된 최신 알파는 타입 인지 린팅 알파 발표를 참고하세요.


oxlint에 타입 인지 린팅이 들어왔습니다!

오래 기다리셨을 no-floating-promises와 관련 규칙이 포함되었습니다.

이 프리뷰 릴리스는 설계 과정과 기술적 세부를 문서로 공유하며 커뮤니티와 협업·토론하기 위한 것입니다.

Quick Start

이미 oxlint가 설정되어 있다면 oxlint-tsgolint를 설치하고 --type-aware 플래그로 실행합니다.

bash
pnpm add -D oxlint-tsgolint@latest
pnpm dlx oxlint --type-aware

설정은 없지만 no-floating-promises만 보고 싶다면:

bash
pnpm add -D oxlint-tsgolint@latest
pnpm dlx oxlint@latest --type-aware -A all -D typescript/no-floating-promises

기대되는 출력 예:

js
 × typescript-eslint(no-floating-promises): Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.
   ╭─[packages/rolldown/src/api/watch/watcher.ts:30:7]
29await this.close();
30originClose();
   ·       ──────────────
31 │     };
   ╰────

설정 옵션은 사용 가이드를 참고하세요.

Performance

저희 테스트에 따르면 예전에 typescript-eslint로 1분 걸리던 저장소가 이제 10초 미만에 끝납니다.

이는 Go로 작성된 10배 빠른 TypeScripttypescript-go를 활용했기 때문입니다.

oxc-ecosystem-ci 프로젝트 기준:

ProjectFilesTime
napi-rs1441.0s
preact2452.7s
rolldown3141.5s
bluesky11527.0s

Type-Aware Linting

생태계에서 타입 인지 린팅 현황을 이해하려면 Rust-Based JavaScript Linters: Fast, But No Typed Linting Right Now 를 참고하세요.

Technical Details

새 기능의 핵심은 oxc-project/tsgolint입니다.

tsgolint는 처음에 typescript-eslint/tsgolint로 프로토타입되었습니다. 다만 typescript-eslint 팀은 ESLLint로의 타입 린팅 작업을 이어갈 계획이라 이 프로토타입에는 개발 자원을 더 넣지 않기로 했습니다.

@boshen@auvred에게 연락해, oxlint에 맞게 범위를 줄인 포크를 만들었습니다. 풀린터 수준의 설정 해석 대신 타입 인지 규칙만 담는 형태입니다.

@auvred께서 Oxc 조직 아래에서 개발을 이어 주시기로 했습니다.

Architecture

oxlint(Rust)와 tsgolint(Go)는 각각 별도 바이너리로 빌드됩니다.

oxlinttsgolint의 "프론트엔드"로 CLI, 경로 순회, 무시 로직, 진단 출력을 담당합니다.

tsgolintoxlint의 백엔드로, 경로와 설정을 입력받아 구조화된 진단을 냅니다.

파이프라인은 단순합니다.

oxlint CLI (경로 + 규칙 + 설정 반환)
  -> tsgolint (진단 반환)
  -> oxlint CLI (진단 출력)

tsgolint

tsgolint는 typescript-go 공개 API로 통신하지 않습니다.

대신 내부 API를 공개용으로 shim해서 typescript-go를 빌드합니다.

모든 타입 인지 규칙은 이 shim API에 직접 작성됩니다.

내부에 의존하는 권장 방식은 아니지만, 동작합니다!

Decision Process

자체 타입 체커 작성

이전에 포기된 시도:

또한 자체 타입 추론을 개발 중인 Biome 2.0도 있습니다.

TypeScript처럼 빠르게 변하는 대상을 따라가며 자체 타입 추론기·체커를 만드는 것은 현실적이지 않다고 판단했습니다.

TypeScript 컴파일러와의 연동

typescript-go 이전에는 공개 API로 TypeScript AST를 estree에 매핑하거나 TS AST를 직접 순회하는 플러그인 방식이 일반적이었습니다. 예:

oxlint와의 프로세스 간 통신도 검토했으나 포기했습니다.

typescript-go에서는 팀이 TypeScript AST를 인코딩하고 JS 쪽에서 프로세스 간 통신으로 디코딩하는 방향을 선호하는 흐름입니다.

이 접근도 여전히 다음 문제가 있습니다.

  • Oxlint 성격에 맞지 않을 정도로 성능 부담이 다름
  • TypeScript AST 매핑 유지 비용

Considerations

tsgolint가 성능 문제는 풀지만, 다른 기술적 과제는 남아 있습니다.

다른 TypeScript 버전 요구

typescript-go 버전의 스냅샷을 배포하고 TypeScript 버전 번호와 맞출 계획입니다. 그러면 맞는 TypeScript 버전으로 oxlint-typescript를 설치할 수 있게 됩니다.

단점은 oxlint-tsgolint 변경에 따라 TypeScript를 올려야 할 수 있다는 점입니다.

tsgolint 유지 비용

TypeScript 내부 API를 shim하는 일은 리스크가 있습니다. 다만 TypeScript AST와 비지터는 꽤 안정적입니다. 이 리스크는 감수하고 typescript-go 업그레이드 시 깨지는 부분을 고칩니다.

typescript-go 포크는 매일 동기화합니다.

Performance Issues

tsgolint는 프로젝트가 수백 개이거나 project reference가 많은 대형 모노레포에서는 아직 성능이 좋지 않을 수 있습니다.

버그가 나면 교착으로 멈추거나 OOM이 날 수 있습니다.

이 문제를 적극적으로 다루며 typescript-go에도 프로파일링·개선을 제출하고 있어, 모든 typescript-go 사용자에게 이득이 됩니다.

코어 팀 @camc314는 이미 여러 PR으로 여러 코드 경로를 크게 빠르게 만들었습니다.

v1.0 Release

tsgolint v1.0에서는 다음을 다룹니다.

  • 대형 모노레포 성능
  • 규칙별 설정
  • 규칙별 정확도
  • IDE 지원
  • 전반적 안정성

Acknowledgements

감사드립니다.

  • typescript-go를 만든 TypeScript 팀
  • 따뜻한 지원을 보내 주신 typescript-eslint
  • tsgolint를 만든 @auvred
  • oxlint + tsgolint 통합을 만든 @camchenry
  • 성능 이슈에 기여한 @camc314

Join the Community

oxlint와 타입 인지 린팅에 대한 피드백을 기다립니다.

연락처:

Next steps

oxlint 설치:

bash
pnpm add -D oxlint@latest oxlint-tsgolint@latest
pnpm dlx oxlint --init # .oxlintrc.json 생성

또는 설치 가이드를 따르세요.

CLI에 --type-aware 플래그를 사용합니다.

bash
pnpm dlx oxlint --type-aware

.oxlintrc.json의 타입 인지 규칙들을 자유롭게 시험해 보세요.

json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "rules": {
    "typescript/await-thenable": "error",
    "typescript/no-array-delete": "error",
    "typescript/no-base-to-string": "error",
    "typescript/no-confusing-void-expression": "error",
    "typescript/no-duplicate-type-constituents": "error",
    "typescript/no-floating-promises": "error",
    "typescript/no-for-in-array": "error",
    "typescript/no-implied-eval": "error",
    "typescript/no-meaningless-void-operator": "error",
    "typescript/no-misused-promises": "error",
    "typescript/no-misused-spread": "error",
    "typescript/no-mixed-enums": "error",
    "typescript/no-redundant-type-constituents": "error",
    "typescript/no-unnecessary-boolean-literal-compare": "error",
    "typescript/no-unnecessary-template-expression": "error",
    "typescript/no-unnecessary-type-arguments": "error",
    "typescript/no-unnecessary-type-assertion": "error",
    "typescript/no-unsafe-argument": "error",
    "typescript/no-unsafe-assignment": "error",
    "typescript/no-unsafe-call": "error",
    "typescript/no-unsafe-enum-comparison": "error",
    "typescript/no-unsafe-member-access": "error",
    "typescript/no-unsafe-return": "error",
    "typescript/no-unsafe-type-assertion": "error",
    "typescript/no-unsafe-unary-minus": "error",
    "typescript/non-nullable-type-assertion-style": "error",
    "typescript/only-throw-error": "error",
    "typescript/prefer-promise-reject-errors": "error",
    "typescript/prefer-reduce-type-parameter": "error",
    "typescript/prefer-return-this-type": "error",
    "typescript/promise-function-async": "error",
    "typescript/related-getter-setter-pairs": "error",
    "typescript/require-array-sort-compare": "error",
    "typescript/require-await": "error",
    "typescript/restrict-plus-operands": "error",
    "typescript/restrict-template-expressions": "error",
    "typescript/return-await": "error",
    "typescript/switch-exhaustiveness-check": "error",
    "typescript/unbound-method": "error",
    "typescript/use-unknown-in-catch-callback-variable": "error"
  }
}