This page documents the refactoring subsystem of the TypeScript language service: how refactorings are registered, what context they receive, how they produce text edits, and the specifics of each built-in refactoring implementation. For related functionality that produces single-error-targeted fixes rather than user-initiated structural changes, see the Code Fixes and Diagnostics page (4.2).
Refactorings are structural code transformations that are triggered explicitly by the user (via a menu or keyboard shortcut) rather than in response to a diagnostic. Each refactoring exposes two operations:
getAvailableActions — given a cursor position or selection range, returns a list of applicable actions with human-readable descriptions.getEditsForAction — given a specific action name, computes and returns the actual file text changes.The framework separates these two phases so editors can present a menu of choices before doing expensive computation.
Lifecycle flow:
Sources: src/services/refactors/extractSymbol.ts180-187 src/services/refactors/moveToNewFile.ts43-76 src/services/refactors/extractType.ts95-165
Each refactoring is registered via registerRefactor, imported from ../refactorProvider.ts. The call shape is:
The kinds array contains hierarchical dot-separated strings (e.g., "refactor.extract.constant", "refactor.rewrite.import.named"). These allow editors to filter which refactors to surface.
Sources: src/services/refactors/extractSymbol.ts168-187 src/services/refactors/convertImport.ts55-103 src/services/refactors/addOrRemoveBracesToArrowFunction.ts52-56
RefactorContextRefactorContext carries all information a refactoring needs:
| Field | Type | Purpose |
|---|---|---|
file | SourceFile | The file being edited |
program | Program | Current compilation program |
startPosition | number | Start of selection/cursor |
endPosition | number | undefined | End of selection (may be absent) |
preferences | UserPreferences | Editor user preferences |
host | LanguageServiceHost | Access to file system / compiler host |
triggerReason | "invoked" | "implicit" | Whether user explicitly invoked vs. ambient triggering |
cancellationToken | CancellationToken | undefined | For cancellable long operations |
kind | string | undefined | Requested kind filter |
The triggerReason field is important: when "invoked", implementations expand their search range to be more permissive (e.g., extractSymbol.ts uses this to scan beyond a cursor into a wider node).
Sources: src/services/refactors/extractSymbol.ts195-198 src/services/refactors/moveToNewFile.ts47-55 src/services/refactors/extractType.ts101-102
ApplicableRefactorInfoReturned by getAvailableActions. The structure is:
ApplicableRefactorInfo {
name: string // Refactor name (e.g., "Extract Symbol")
description: string // Human-readable description
actions: RefactorActionInfo[]
}
RefactorActionInfo {
name: string // Action name used in getEditsForAction
description: string
kind: string // Hierarchical kind string
notApplicableReason?: string // Filled when provideRefactorNotApplicableReason = true
range?: { start, end } // Optional highlight range in the editor
}
When UserPreferences.provideRefactorNotApplicableReason is true, implementations return actions with notApplicableReason filled instead of returning an empty array, so the editor can display why an option is unavailable.
Sources: src/services/refactors/extractSymbol.ts200-333 src/services/refactors/addOrRemoveBracesToArrowFunction.ts65-92
RefactorEditInfoReturned by getEditsForAction:
RefactorEditInfo {
edits: FileTextChanges[] // Text changes across one or more files
renameFilename?: string // File to trigger a rename in
renameLocation?: number // Position of the symbol to rename after edit
}
When a refactoring creates a new symbol (e.g., Extract Constant creates a new variable), renameFilename and renameLocation direct the editor to immediately place the user in rename mode on the newly created name.
Sources: src/services/refactors/extractType.ts160-163 src/services/refactors/generateGetAccessorAndSetAccessor.ts34-39 src/services/refactors/addOrRemoveBracesToArrowFunction.ts122-124
ChangeTrackerAll refactorings build their edits through textChanges.ChangeTracker (src/services/textChanges.ts493-507). The typical pattern is:
ChangeTracker.with returns FileTextChanges[] ready for inclusion in RefactorEditInfo.edits. Key methods:
| Method | Purpose |
|---|---|
replaceNode | Swap one AST node for another |
insertNodeBefore / insertNodeAfter | Insert a new node relative to an existing one |
deleteNode / delete | Remove a node (with trivia-aware options) |
replaceRangeWithText | Raw text replacement |
insertStatementsInNewFile | Emit statements into a brand-new file |
Sources: src/services/textChanges.ts493-510 src/services/textChanges.ts565-591
Sources: src/services/refactors/extractSymbol.ts180-187 src/services/refactors/moveToNewFile.ts43-44 src/services/textChanges.ts493-510
File: src/services/refactors/extractSymbol.ts
Registered name: "Extract Symbol"
Actions:
| Action name | Kind | Description |
|---|---|---|
"Extract Constant" | refactor.extract.constant | Extract selected expression to a const |
"Extract Function" | refactor.extract.function | Extract selected code to a new function |
Step 1: Determine the extraction range
getRangeToExtract (src/services/refactors/extractSymbol.ts458-542) takes a TextSpan and resolves it to a TargetRange:
RangeToExtract = { targetRange: TargetRange } | { errors: Diagnostic[] }
TargetRange {
range: Expression | Statement[]
facts: RangeFacts
thisNode: Node | undefined
}
RangeFacts is a bitmask that records properties of the selected range:
| Flag | Meaning |
|---|---|
HasReturn | Range contains a return statement |
IsGenerator | Range is in a generator function |
IsAsyncFunction | Range is in an async function |
UsesThis | Range references this |
UsesThisInFunction | this is from a regular function context |
InStaticRegion | Range is inside a static class member |
Step 2: Enumerate possible extraction scopes
getPossibleExtractions walks up the scope chain from the selected range and returns a list of possible scopes (innermost first). Each scope yields a functionExtraction and constantExtraction candidate with either a description or an array of errors explaining why extraction to that scope is impossible.
Step 3: Build actions
getRefactorActionsToExtractSymbol (src/services/refactors/extractSymbol.ts195-343) iterates the possible extractions and builds RefactorActionInfo items. Actions are de-duplicated by description (since multiple scopes may produce the same description), and scopes are numbered function_scope_0, function_scope_1, ... so the index can be recovered when getEditsForAction is called.
Step 4: Generate edits
getRefactorEditsToExtractSymbol (src/services/refactors/extractSymbol.ts350-368) parses the action name's index, re-runs getRangeToExtract, and calls either getFunctionExtractionAtIndex or getConstantExtractionAtIndex. These generate:
Error messages for why extraction is blocked are defined in the Messages namespace (src/services/refactors/extractSymbol.ts373-401), e.g.:
cannotExtractImport — import statements cannot be extractedcannotExtractRangeContainingConditionalReturnStatement — conditional returns make extraction unsafecannotAccessVariablesFromNestedScopes — nested function closures block extractionSources: src/services/refactors/extractSymbol.ts168-401 src/services/refactors/extractSymbol.ts403-430 src/services/refactors/extractSymbol.ts458-542
File: src/services/refactors/extractType.ts
Registered name: "Extract type"
Actions:
| Action name | Kind | Applies when |
|---|---|---|
"Extract to type alias" | refactor.extract.type | TypeScript files |
"Extract to interface" | refactor.extract.interface | Selection is a type literal / intersection of type literals |
"Extract to typedef" | refactor.extract.typedef | JavaScript files (emits @typedef JSDoc) |
getRangeToExtract (src/services/refactors/extractType.ts185-217) identifies the TypeNode (or array of type nodes for union/intersection selections) under the cursor. It then:
collectTypeParameters to find all type parameters from the enclosing scope that are referenced in the selection. These become type parameters on the new alias/interface.flattenTypeLiteralNodeReference to determine whether the selection resolves to a set of TypeElements that qualify for interface extraction.The three mutation functions are:
doTypeAliasChange — inserts a type NewType<T> = ... alias before the enclosing statement, replaces the original type with NewType<T>.doInterfaceChange — inserts an interface NewType<T> { ... } declaration.doTypedefChange — inserts a @typedef JSDoc tag for JS files.All three use getUniqueName("NewType", file) to avoid collisions, and return renameLocation pointing at the inserted name.
Sources: src/services/refactors/extractType.ts77-165 src/services/refactors/extractType.ts185-217 src/services/refactors/extractType.ts338-417
File: src/services/refactors/moveToNewFile.ts
Registered name: "Move to a new file" | Kind: refactor.move.newFile
This refactoring requires UserPreferences.allowTextChangesInNewFiles = true to be active, as it creates a new file.
getAvailableActions calls getStatementsToMove(context) to determine if the selection covers top-level statements eligible for extraction. An additional guard prevents triggering implicitly when both the start and end tokens are inside nested blocks.
getEditsForAction (src/services/refactors/moveToNewFile.ts70-75) delegates to doChange:
getUsageInfo(oldFile, toMove.all, checker) — analyzes which symbols the moved statements import from the old file, and which symbols the old file still uses from the moved code.createNewFileName(oldFile, program, host, toMove) — derives a filename from the first moved declaration.createFutureSourceFile — creates a synthetic SourceFile representing the new file (needed for import resolution).getNewStatementsAndRemoveFromOldFile — uses ChangeTracker to:
import/export declarations to the old file.addNewFileToTsconfig — adds the new file path to tsconfig.json if one is present.The result has renameFilename: undefined since no rename is needed post-move.
Sources: src/services/refactors/moveToNewFile.ts43-88
File: src/services/refactors/generateGetAccessorAndSetAccessor.ts
Registered name: "Generate 'get' and 'set' accessors" | Kind: refactor.rewrite.property.generateAccessors
This refactoring converts a class property (or constructor parameter property, or object literal property) into a private backing field plus get/set accessor pair.
getAvailableActions calls codefix.getAccessorConvertiblePropertyAtPosition (src/services/codefixes/generateAccessors.ts166-212), which:
AcceptedDeclaration (ParameterPropertyDeclaration | PropertyDeclaration | PropertyAssignment).AccessorInfo or a RefactorErrorInfo.getEditsForAction calls codefix.generateAccessorFromProperty (src/services/codefixes/generateAccessors.ts78-128), which:
_).get accessor that returns this._field.set accessor (unless readonly).readonly properties, updates constructor assignments to use the new field name.The renameLocation points at the renamed field or accessor name so the user can immediately rename it.
Sources: src/services/refactors/generateGetAccessorAndSetAccessor.ts26-65 src/services/codefixes/generateAccessors.ts56-128
File: src/services/refactors/convertExport.ts
Registered name: "Convert export"
Actions:
| Action name | Kind | Converts |
|---|---|---|
"Convert default export to named export" | refactor.rewrite.export.named | export default X → export { X } |
"Convert named export to default export" | refactor.rewrite.export.default | export const X → export default X |
getInfo (src/services/refactors/convertExport.ts121-177) identifies the export declaration at the cursor and returns ExportInfo with wasDefault, exportNode, exportName, and exportingModuleSymbol.
doChange calls two sub-functions:
changeExport — rewrites the export syntax in the exporting file.changeImports — uses FindAllReferences.Core.eachExportReference to find all importing files and update their import syntax accordingly.Guarded: if the file already has a default export and the operation would create a second one, the action is rejected with "This file already has a default export".
Sources: src/services/refactors/convertExport.ts61-110 src/services/refactors/convertExport.ts121-238
File: src/services/refactors/convertImport.ts
Registered name: "Convert import"
Actions:
| Action name | Kind | Converts |
|---|---|---|
"Convert namespace import to named imports" | refactor.rewrite.import.named | import * as m → import { x, y } |
"Convert named imports to namespace import" | refactor.rewrite.import.namespace | import { x, y } → import * as m |
"Convert named imports to default import" | refactor.rewrite.import.default | import { x } → import x |
getImportConversionInfo (src/services/refactors/convertImport.ts111-139) locates the import declaration and determines which conversion is applicable based on whether the binding is a NamespaceImport or NamedImports. When converting namespace to named, FindAllReferences.Core.eachSymbolReferenceInFile is used to find all usages of m.x expressions so they can be rewritten to plain x identifiers.
The doChangeNamedToNamespaceOrDefault function is exported and also used by other code paths.
Sources: src/services/refactors/convertImport.ts55-103 src/services/refactors/convertImport.ts111-253
File: src/services/refactors/addOrRemoveBracesToArrowFunction.ts
Registered name: "Add or remove braces in an arrow function"
Actions:
| Action name | Kind | Converts |
|---|---|---|
"Add braces to arrow function" | refactor.rewrite.arrow.braces.add | x => expr → x => { return expr; } |
"Remove braces from arrow function" | refactor.rewrite.arrow.braces.remove | x => { return expr; } → x => expr |
getConvertibleArrowFunctionAtPosition (src/services/refactors/addOrRemoveBracesToArrowFunction.ts126-157) finds the enclosing arrow function and checks:
Expression (concise body).Block with exactly one ReturnStatement.When removing braces, comments are carefully transferred between the return statement and the new concise body using copyLeadingComments, copyTrailingComments, and copyTrailingAsLeadingComments.
Object literal expressions in concise bodies are wrapped in parentheses to avoid { being parsed as a block: x => ({ key: val }).
Sources: src/services/refactors/addOrRemoveBracesToArrowFunction.ts39-157
The kind strings form a namespace hierarchy. Editors can request only a subtree by prefix-matching. The refactorKindBeginsWith utility (from refactorProvider.ts) implements this check.
refactor
├── extract
│ ├── constant (Extract Symbol → Extract Constant)
│ ├── function (Extract Symbol → Extract Function)
│ ├── type (Extract Type → type alias)
│ ├── interface (Extract Type → interface)
│ └── typedef (Extract Type → typedef JSDoc)
├── move
│ └── newFile (Move to New File)
└── rewrite
├── export
│ ├── named (Convert Export → default→named)
│ └── default (Convert Export → named→default)
├── import
│ ├── named (Convert Import → namespace→named)
│ ├── namespace (Convert Import → named→namespace)
│ └── default (Convert Import → named→default)
├── property
│ └── generateAccessors (Generate Get/Set)
└── arrow
└── braces
├── add (Add braces)
└── remove (Remove braces)
Sources: src/services/refactors/extractSymbol.ts169-179 src/services/refactors/extractType.ts79-93 src/services/refactors/moveToNewFile.ts38-42 src/services/refactors/convertExport.ts63-73 src/services/refactors/convertImport.ts57-73 src/services/refactors/generateGetAccessorAndSetAccessor.ts21-25 src/services/refactors/addOrRemoveBracesToArrowFunction.ts42-51
ChangeTracker Trivia HandlingText edits must handle whitespace and comments carefully. ChangeTracker provides LeadingTriviaOption and TrailingTriviaOption enums:
LeadingTriviaOption | Behavior |
|---|---|
Exclude | Use node.getStart() — skip all leading trivia |
IncludeAll | Include leading trivia and inline preceding trivia |
JSDoc | Include attached JSDoc comment block |
StartLine | Only trivia on the same line as the node start |
TrailingTriviaOption | Behavior |
|---|---|
Exclude | Use node.end exactly |
ExcludeWhitespace | Strip whitespace but keep comments |
Include | Include trailing line break |
Refactorings typically use useNonAdjustedPositions (both Exclude) when replacing a node in-place, and LeadingTriviaOption.IncludeAll when deleting a statement.
Sources: src/services/textChanges.ts213-241 src/services/textChanges.ts277-280 src/services/textChanges.ts533-556
RefactorErrorInfo PatternWhen a refactoring is not applicable at the given position, implementations return a RefactorErrorInfo with an error: string instead of a valid info object. The isRefactorErrorInfo guard function distinguishes the two. This allows callers to either silently skip (when provideRefactorNotApplicableReason is false) or surface the reason (when it is true).
Sources: src/services/refactors/addOrRemoveBracesToArrowFunction.ts65-92 src/services/refactors/extractType.ts101-137 src/services/refactors/convertImport.ts77-95
src/services/refactors/
├── extractSymbol.ts — Extract Function / Extract Constant
├── extractType.ts — Extract Type Alias / Interface / Typedef
├── moveToNewFile.ts — Move to New File
├── generateGetAccessorAndSetAccessor.ts — Generate Get/Set Accessors
├── convertExport.ts — Convert Default ↔ Named Export
├── convertImport.ts — Convert Namespace ↔ Named ↔ Default Import
└── addOrRemoveBracesToArrowFunction.ts — Add/Remove Arrow Function Braces
src/services/codefixes/
└── generateAccessors.ts — Shared accessor generation logic (used by refactor + code fix)
src/services/textChanges.ts — ChangeTracker: accumulates and applies all edits
Sources: src/services/refactors/extractSymbol.ts1-5 src/services/codefixes/generateAccessors.ts55-64 src/services/textChanges.ts493-507
Refresh this wiki
This wiki was recently refreshed. Please wait 4 days to refresh again.