PageSourceSearch

https://typescript-eslint.io/assets/js/a1c09ed5.37f25e53.js

js typescript-eslint.io collected 2026-10-02 02:18:06 UTC 18,040 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([["4795"],{36033(e,t,r){r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>i,metadata:()=>n,toc:()=>l});var n=r(59533),o=r(65723),a=r(7143);let i={authors:"bradzacher",description:"Changes to consistent-type-imports when used with decorators, experimentalDecorators, and emitDecoratorMetadata",slug:"changes-to-consistent-type-imports-with-decorators",tags:["consistent-type-imports","experimentalDecorators","emitDecoratorMetadata","typescript-eslint"],title:"Changes to `consistent-type-imports` with Legacy Decorators and Decorator Metadata"},s,c={authorsImageUrls:[void 0]},l=[{value:"Experimental Decorator Metadata",id:"experimental-decorator-metadata",level:2},{value:"<code>consistent-type-imports</code> caused runtime breakage",id:"consistent-type-imports-caused-runtime-breakage",level:2},{value:"Past (Broken) Solution",id:"past-broken-solution",level:2},{value:"Today&#39;s Solution - the Compromise",id:"todays-solution---the-compromise",level:2},{value:"Configuring the linter to expect <code>experimentalDecorators: true</code> and <code>emitDecoratorMetadata: true</code>",id:"configuring-the-linter-to-expect-experimentaldecorators-true-and-emitdecoratormetadata-true",level:3},{value:"Alternatives for Impacted Users",id:"alternatives-for-impacted-users",level:2},{value:"Supporting typescript-eslint",id:"supporting-typescript-eslint",level:2}];function d(e){let t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(t.p,{children:["We've made some changes to the ",(0,o.jsxs)(t.a,{href:"/rules/consistent-type-imports",children:[(0,o.jsx)(t.code,{children:"consistent-type-imports"})," rule"]})," to fix some long-standing issues when used alongside ",(0,o.jsx)(t.code,{children:"experimentalDecorators: true"})," and ",(0,o.jsx)(t.code,{children:"emitDecoratorMetadata: true"}),". These changes increase safety and prevent invalid fixes when using decorator metadata."]}),"\n",(0,o.jsx)(t.h2,{id:"experimental-decorator-metadata",children:"Experimental Decorator Metadata"}),"\n",(0,o.jsxs)(t.p,{children:["TypeScript's ",(0,o.jsxs)(t.a,{href:"https://aka.ms/tsconfig#experimentalDecorators",children:[(0,o.jsx)(t.code,{children:"experimentalDecorators"})," compiler option"]}),' (referred to as "legacy decorators" from here on) turns on support for an old version of the ',(0,o.jsx)(t.a,{href:"https://github.com/tc39/proposal-decorators",children:"JavaScript TC39 decorator proposal"})," that was never standardized. TypeScript's legacy decorators are similar to the current proposal, but differ in that they use ",(0,o.jsx)(t.a,{href:"https://rbuckton.github.io/reflect-metadata/",children:"metadata reflection"})," when TypeScript's ",(0,o.jsxs)(t.a,{href:"https://aka.ms/tsconfig#emitDecoratorMetadata",children:[(0,o.jsx)(t.code,{children:"emitDecoratorMetadata"})," compiler option"]})," is turned on."]}),"\n",(0,o.jsx)(t.p,{children:"When using legacy decorators with decorator metadata and a class is annotated with decorators, TypeScript will emit runtime metadata for the class (see the example below). That decorator metadata will capture property types, method parameter types, and method return types. Decorator metadata provides a bridge between the types (which are not available at compile time) and the runtime code."}),"\n",(0,o.jsxs)(t.p,{children:["The downside of generating this runtime code that it is derived using type information: meaning that the runtime code emitted changes based on the cross-file type information that TypeScript has computed. Doing so violates a key ",(0,o.jsx)(t.a,{href:"https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals",children:"TypeScript design goal"})," of not changing runtime behavior based on type information."]}),"\n",(0,o.jsx)(t.p,{children:"To illustrate what this means consider the following snippet:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import Foo from 'foo';\nimport decorator from 'decorator';\n\nclass Clazz {\n  @decorator\n  method(arg: Foo) {}\n}\n"})}),"\n",(0,o.jsx)(t.p,{children:"TypeScript will transpile this code to the following:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:'import { __decorate, __metadata } from "tslib";\nimport Foo from \'foo\';\nimport decorator from \'decorator\';\nclass Clazz {\n    method(arg) { }\n}\n__decorate([\n    decorator,\n    __metadata("design:type", Function),\n    __metadata("design:paramtypes", /* See below for what this value will be */),\n    __metadata("design:returntype", void 0)\n], Clazz.prototype, "method", null);\n'})}),"\n",(0,o.jsxs)(t.p,{children:["If the imported name ",(0,o.jsx)(t.code,{children:"Foo"})," resolves to..."]}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:["a type then TS will emit ",(0,o.jsx)(t.code,{children:"[Function]"}),", ",(0,o.jsx)(t.code,{children:"[Object]"}),", ",(0,o.jsx)(t.code,{children:"[String]"}),", ",(0,o.jsx)(t.code,{children:"[Number]"}),", or ",(0,o.jsx)(t.code,{children:"[Boolean]"})," depending on what that type resolves to."]}),"\n",(0,o.jsxs)(t.li,{children:["an enum then TS will emit one of ",(0,o.jsx)(t.code,{children:"[String]"}),", ",(0,o.jsx)(t.code,{children:"[Number]"}),", or ",(0,o.jsx)(t.code,{children:"[Object]"})," depending on the type of the enum's members","\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"[Object]"})," is used for an enum that has both string and number values."]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(t.li,{children:["a class declaration:","\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:["and the import ",(0,o.jsx)(t.strong,{children:(0,o.jsx)(t.em,{children:"is NOT"})})," annotated as ",(0,o.jsx)(t.code,{children:"import type"})," then TS will emit ",(0,o.jsx)(t.code,{children:"[Foo]"}),"."]}),"\n",(0,o.jsxs)(t.li,{children:["and the import ",(0,o.jsx)(t.strong,{children:(0,o.jsx)(t.em,{children:"IS"})})," annotated as ",(0,o.jsx)(t.code,{children:"import type"})," then TS will emit ",(0,o.jsx)(t.code,{children:"[Function]"}),"."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,o.jsx)(t.p,{children:"In addition to requiring runtime type information, those metadata emit rules are confusing for developers to reason about.\nThey necessitate understanding edge cases specific to TypeScript's handling of decorators and type information."}),"\n",(0,o.jsxs)(t.h2,{id:"consistent-type-imports-caused-runtime-breakage",children:[(0,o.jsx)(t.code,{children:"consistent-type-imports"})," caused runtime breakage"]}),"\n",(0,o.jsxs)(t.p,{children:["The important piece is that last dot point above - the handling of imported names that resolve to class declarations. If the import is not annotated as ",(0,o.jsx)(t.code,{children:"import type"})," then TS emits a runtime reference to the imported name. This runtime reference is implicit and requires type information to derive - you cannot derive its existence purely based on single-file AST analysis."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsxs)(t.a,{href:"/rules/consistent-type-imports",children:[(0,o.jsx)(t.code,{children:"consistent-type-imports"})," rule"]})," was introduced to allow users to enforce that any imported names are annotated as ",(0,o.jsx)(t.code,{children:"import type"}),' if they are not used in a value location. How the rule makes this decision is based solely on the single file it\'s looking at. But another way the rule does not use any type information from TS and instead it scans the code using a technique called "scope analysis" so that it can find all references to the imported names and determine if each reference is a value reference.']}),"\n",(0,o.jsx)(t.admonition,{type:"tip",children:(0,o.jsxs)(t.p,{children:["See ",(0,o.jsx)(t.a,{href:"/blog/asts-and-typescript-eslint",children:"ASTs and typescript-eslint"})," to understand how rules look at the syntax of files."]})}),"\n",(0,o.jsxs)(t.p,{children:["The issue arises with legacy decorators and decorator metadata - syntactically the only reference to ",(0,o.jsx)(t.code,{children:"Foo"})," is a type reference. However the emitted code contains a hidden reference to ",(0,o.jsx)(t.code,{children:"Foo"}),". When the rule relies upon the code it sees then it will report an error and attempt to mark ",(0,o.jsx)(t.code,{children:"Foo"})," as ",(0,o.jsx)(t.code,{children:"import type"}),". If the user applies this fix then that will cause their runtime code to change (",(0,o.jsx)(t.code,{children:"arg"}),"'s metadata goes from ",(0,o.jsx)(t.code,{children:"Foo"})," to ",(0,o.jsx)(t.code,{children:"Function"}),") which can have downstream runtime impacts and cause broken code!"]}),"\n",(0,o.jsx)(t.h2,{id:"past-broken-solution",children:"Past (Broken) Solution"}),"\n",(0,o.jsxs)(t.p,{children:["In the past we tried to solve this problem by enforcing that imported names that are used in decorator metadata are specifically ",(0,o.jsx)(t.em,{children:"not"})," marked as type-only imports to ensure that values are always correctly emitted in the runtime code."]}),"\n",(0,o.jsxs)(t.p,{children:["However this solution had a hidden pitfall; if the user also used ",(0,o.jsx)(t.code,{children:"isolatedModules: true"})," then TS will enforce that all imported types are explicitly marked as ",(0,o.jsx)(t.code,{children:"import type"})," for compatibility with single-file build tools. This lead to an unresolvable situation where ",(0,o.jsx)(t.code,{children:"consistent-type-imports"})," would enforce that an imported name ",(0,o.jsx)(t.em,{children:"must not be"})," marked with ",(0,o.jsx)(t.code,{children:"import type"})," so that we could ensure we don't break decorator metadata, and simultaneously TS would enforce that that same imported name ",(0,o.jsx)(t.em,{children:"must be"})," marked with ",(0,o.jsx)(t.code,{children:"import type"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["There have been a few attempts to fix this issue but the resolution we came to was that the only solution was to add type information to the rule so that it could correctly understand all of the above type-aware constraints. Adding type information to an existing rule is something we try to avoid because it is a major breaking change that restricts the rule to just users that leverage ",(0,o.jsx)(t.a,{href:"/getting-started/typed-linting",children:"type-aware linting"}),"."]}),"\n",(0,o.jsx)(t.p,{children:"Adding type-information to the rule to handle this edge c
1ase would not be a positive change for users or the ecosystem:"}),"\n",(0,o.jsxs)(t.ol,{children:["\n",(0,o.jsxs)(t.li,{children:["Many users are unable to configure typed linting and/or unwilling to take its performance hit. Requiring ",(0,o.jsx)(t.a,{href:"/getting-started/typed-linting",children:"type-aware linting"})," linting for this rule to serve a very small subset of impacted users would reduce the linting ability of many more un-impacted users."]}),"\n",(0,o.jsx)(t.li,{children:"It requires a specific combinations of compiler options to trigger it means that not everyone is impacted by the problem - so we'd be preventing a lot of un-impacted users from using the rule."}),"\n",(0,o.jsxs)(t.li,{children:["With the release of ",(0,o.jsx)(t.a,{href:"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators",children:"TypeScript v5.0 and its stable decorators"})," ",(0,o.jsx)(t.code,{children:"experimentalDecorators"})," are now the legacy syntax. Whilst ",(0,o.jsx)(t.a,{href:"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata",children:"TypeScript v5.2 added support for the latest stable decorator metadata proposal"})," this proposal does not include type metadata - so it doesn't suffer the same drawbacks as its legacy counterpart."]}),"\n"]}),"\n",(0,o.jsx)(t.h2,{id:"todays-solution---the-compromise",children:"Today's Solution - the Compromise"}),"\n",(0,o.jsx)(t.p,{children:"Ultimately we determined the best solution was to just opt-out of handling this use-case entirely. This means that we can avoid accidentally reporting the wrong thing and fixing to code that either fails to compile or alters the emitted runtime metadata."}),"\n",(0,o.jsxs)(t.p,{children:["Now, if you have ",(0,o.jsx)(t.strong,{children:"both"})," ",(0,o.jsx)(t.code,{children:"experimentalDecorators: true"})," and ",(0,o.jsx)(t.code,{children:"emitDecoratorMetadata: true"}),", then the ",(0,o.jsx)(t.code,{children:"consistent-type-imports"})," rule will ",(0,o.jsx)(t.strong,{children:(0,o.jsx)(t.em,{children:"not"})})," report any errors within any files ",(0,o.jsx)(t.em,{children:"that contain decorators"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["All files without decorators will continue to report as expected. Similarly all projects that use ",(0,o.jsx)(t.code,{children:"experimentalDecorators: false"})," and/or ",(0,o.jsx)(t.code,{children:"emitDecoratorMetadata: false"})," will continue to report as expected."]}),"\n",(0,o.jsxs)(t.h3,{id:"configuring-the-linter-to-expect-experimentaldecorators-true-and-emitdecoratormetadata-true",children:["Configuring the linter to expect ",(0,o.jsx)(t.code,{children:"experimentalDecorators: true"})," and ",(0,o.jsx)(t.code,{children:"emitDecoratorMetadata: true"})]}),"\n",(0,o.jsxs)(t.p,{children:["If you are using ",(0,o.jsx)(t.a,{href:"/getting-started/typed-linting",children:"type-aware linting"})," then we will automatically infer your setup from your tsconfig and you should not need to configure anything manually."]}),"\n",(0,o.jsxs)(t.p,{children:["Otherwise you can explicitly tell our tooling to analyze your co
1de as if the compiler option was turned on by setting both ",(0,o.jsx)(t.a,{href:"/packages/parser/#emitdecoratormetadata",children:(0,o.jsx)(t.code,{children:"parserOptions.emitDecoratorMetadata = true"})})," and ",(0,o.jsx)(t.a,{href:"/packages/parser/#experimentaldecorators",children:(0,o.jsx)(t.code,{children:"parserOptions.experimentalDecorators = true"})}),". For example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-js",metastring:'title="eslint.config.js"',children:"import tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n  ...tseslint.configs.recommended,\n  // Added lines start\n  {\n    languageOptions: {\n      parserOptions: {\n        emitDecoratorMetadata: true,\n        experimentalDecorators: true,\n      },\n    },\n  },\n);\n"})}),"\n",(0,o.jsx)(t.h2,{id:"alternatives-for-impacted-users",children:"Alternatives for Impacted Users"}),"\n",(0,o.jsxs)(t.p,{children:["If you are working in a workspace that is impacted by this change and want to correctly have your imports consistently marked with ",(0,o.jsx)(t.code,{children:"type"}),", we suggest using the ",(0,o.jsx)(t.a,{href:"https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax",children:(0,o.jsx)(t.code,{children:"verbatimModuleSyntax"})})," compiler option which will use type information to correctly enforce that types are marked with ",(0,o.jsx)(t.code,{children:"type"})," and values are not when they are used in decorator metadata."]}),"\n",(0,o.jsx)(t.h2,{id:"supporting-typescript-eslint",children:"Supporting typescript-eslint"}),"\n",(0,o.jsxs)(t.p,{children:["If you enjoyed this blog post and/or use typescript-eslint, please consider ",(0,o.jsx)(t.a,{href:"https://opencollective.com/typescript-eslint",children:"supporting us on Open Collective"}),". We're a small volunteer team and could use your support to make the ESLint experience on TypeScript great. Thanks! \u{1F496}"]})]})}function h(e={}){let{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},7143(e,t,r){r.d(t,{R:()=>i,x:()=>s});var n=r(22155);let o={},a=n.createContext(o);function i(e){let t=n.useContext(a);return n.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),n.createElement(a.Provider,{value:t},e.children)}},59533(e){e.exports=JSON.parse('{"permalink":"/blog/changes-to-consistent-type-imports-with-decorators","source":"@site/blog/2024-03-25-changes-to-consistent-type-imports-with-decorators.md","title":"Changes to `consistent-type-imports` with Legacy Decorators and Decorator Metadata","description":"Changes to consistent-type-imports when used with decorators, experimentalDecorators, and emitDecoratorMetadata","date":"2024-03-25T00:00:00.000Z","tags":[{"inline":true,"label":"consistent-type-imports","permalink":"/blog/tags/consistent-type-imports"},{"inline":true,"label":"experimentalDecorators","permalink":"/blog/tags/experimental-decorators"},{"inline":true,"label":"emitDecoratorMetadata","permalink":"/blog/tags/emit-decorator-metadata"},{"inline":true,"label":"typescript-eslint","permalink":"/blog/tags/typescript-eslint"}],"readingTime":6.68,"hasTruncateMarker":true,"authors":[{"name":"Brad Zacher","title":"typescript-eslint Maintainer","url":"https://github.com/bradzacher","imageURL":"/img/team/bradzacher.jpg","key":"bradzacher","page":null}],"frontMatter":{"authors":"bradzacher","description":"Changes to consistent-type-imports when used with decorators, experimentalDecorators, and emitDecoratorMetadata","slug":"changes-to-consistent-type-imports-with-decorators","tags":["consistent-type-imports","experimentalDecorators","emitDecoratorMetadata","typescript-eslint"],"title":"Changes to `consistent-type-imports` with Legacy Decorators and Decorator Metadata"},"unlisted":false,"prevItem":{"title":"Announcing typescript-eslint v8 Beta","permalink":"/blog/announcing-typescript-eslint-v8-beta"},"nextItem":{"title":"Announcing typescript-eslint v7","permalink":"/blog/announcing-typescript-eslint-v7"}}')}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.