📄 consistent-type-exports¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 7 |
| 📦 Imports | 11 |
| 📐 Interfaces | 2 |
| 📑 Type Aliases | 2 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/consistent-type-exports.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'consistent-type-exports' |
meta.type |
'suggestion' |
meta.docs.description |
'Enforce consistent usage of type exports' |
meta.docs.requiresTypeChecking |
true |
meta.fixable |
'code' |
meta.messages.multipleExportsAreTypes |
'Type exports {{exportNames}} are not values and should be exported using export type.' |
meta.messages.singleExportIsType |
'Type export {{exportNames}} is not a value and should be exported using export type.' |
meta.messages.typeOverValue |
'All exports in the declaration are only used as types. Use export type.' |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { fixMixedExportsWithInlineTypeSpecifier: { type: 'boole... |
defaultOptions |
[ { fixMixedExportsWithInlineTypeSpecifier: false, }, ] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
AST_TOKEN_TYPES |
@typescript-eslint/utils |
createRule |
../util |
formatWordList |
../util |
getParserServices |
../util |
isClosingBraceToken |
../util |
isOpeningBraceToken |
../util |
nullThrows |
../util |
NullThrowsReasons |
../util |
Functions¶
create(context: any, [{ fixMixedExportsWithInlineTyp…: any): { ExportAllDeclaration(node: any): void; ExportNamedDeclara…¶
Parameters:
contextany[{ fixMixedExportsWithInlineTypeSpecifier }]any
Returns: { ExportAllDeclaration(node: any): void; ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void; 'Program:exit'(): void; }
Calls:
getParserServices (from ../util)services.program.getTypeCheckerchecker.isUnknownSymbolsymbol.getDeclarations()?.sometsutils.isSymbolFlagSetisSymbolTypeBasedchecker.getImmediateAliasedSymbolts.resolveModuleNameservices.program.getCompilerOptionsservices.program.getSourceFilechecker.getSymbolAtLocationchecker.getTypeOfSymbolchecker .getPropertiesOfType(sourceFileType) .somechecker.getPropertyOfTypepropertyTypeSymbol.escapedName.toStringcontext.reportnullThrows (from ../util)context.sourceCode.getFirstTokenNullThrowsReasons.MissingTokenfixer.insertTextBeforegetSourceFromExportinlineTypeSpecifiers.pushservices.getSymbolAtLocationtypeBasedSpecifiers.pushvalueSpecifiers.pushsourceExports.reportValueExports.pushObject.valuesfixExportInsertTypereport.typeBasedSpecifiers.mapfixAddTypeSpecifierToNamedExportsfixSeparateNamedExportsformatWordList (from ../util)
Internal Comments:
/**
* Helper for identifying if a symbol resolves to a
* JavaScript value or a TypeScript type.
*
* @returns True/false if is a type or not, or undefined if the specifier
* can't be resolved.
*/
// Module can explicitly export types or values, and it's not difficult (x2)
// to distinguish one from the other, since we can get the flags of (x2)
// the exported symbols or check if symbol export declaration has (x2)
// the "type" keyword in it. (x2)
// (x6)
// Things get a lot more complicated when we're dealing with (x2)
// export * from './module-with-type-only-exports' (x2)
// export type * from './module-with-type-and-value-exports' (x2)
// TS checker has an internal function getExportsOfModuleWorker that (x2)
// recursively visits all module exports, including "export *". It then (x2)
// puts type-only-star-exported symbols into the typeOnlyExportStarMap (x2)
// property of sourceFile's SymbolLinks. Since symbol links aren't (x2)
// exposed outside the checker, we cannot access it directly. (x2)
// Therefore, to filter out value properties, we use the following hack: (x2)
// checker.getPropertiesOfType returns all exports that were originally (x2)
// values, but checker.getPropertyOfType returns undefined for (x2)
// properties that are mentioned in the typeOnlyExportStarMap. (x2)
// Coerce the source into a string for use as a lookup entry. (x2)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (x2)
// Cache the first encountered exports for the package. We will need to come
// back to these later when fixing the problems.
// The export is a type export (x4)
// The export is a value export (x4)
// Next for the current export, we will separate type/value specifiers. (x2)
// Note: it is valid to export values as types. We will avoid reporting errors
// when this is encountered.
// When isTypeBased is undefined, we should avoid reporting them. (x4)
// If this export has no issues, move on.
// Export is all type-only with no type specifiers; convert the entire export to `export type`. (x4)
// We have both type and value violations. (x2)
Code
create(context, [{ fixMixedExportsWithInlineTypeSpecifier }]) {
const sourceExportsMap: Record<string, SourceExports> = {};
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
/**
* Helper for identifying if a symbol resolves to a
* JavaScript value or a TypeScript type.
*
* @returns True/false if is a type or not, or undefined if the specifier
* can't be resolved.
*/
function isSymbolTypeBased(
symbol: ts.Symbol | undefined,
): boolean | undefined {
if (!symbol || checker.isUnknownSymbol(symbol)) {
return undefined;
}
if (
symbol.getDeclarations()?.some(ts.isTypeOnlyImportOrExportDeclaration)
) {
return true;
}
if (tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Value)) {
return false;
}
return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias)
? isSymbolTypeBased(checker.getImmediateAliasedSymbol(symbol))
: true;
}
return {
ExportAllDeclaration(node): void {
if (node.exportKind === 'type') {
return;
}
const sourceModule = ts.resolveModuleName(
node.source.value,
context.filename,
services.program.getCompilerOptions(),
ts.sys,
);
if (sourceModule.resolvedModule == null) {
return;
}
const sourceFile = services.program.getSourceFile(
sourceModule.resolvedModule.resolvedFileName,
);
if (sourceFile == null) {
return;
}
const sourceFileSymbol = checker.getSymbolAtLocation(sourceFile);
if (sourceFileSymbol == null) {
return;
}
const sourceFileType = checker.getTypeOfSymbol(sourceFileSymbol);
// Module can explicitly export types or values, and it's not difficult
// to distinguish one from the other, since we can get the flags of
// the exported symbols or check if symbol export declaration has
// the "type" keyword in it.
//
// Things get a lot more complicated when we're dealing with
// export * from './module-with-type-only-exports'
// export type * from './module-with-type-and-value-exports'
//
// TS checker has an internal function getExportsOfModuleWorker that
// recursively visits all module exports, including "export *". It then
// puts type-only-star-exported symbols into the typeOnlyExportStarMap
// property of sourceFile's SymbolLinks. Since symbol links aren't
// exposed outside the checker, we cannot access it directly.
//
// Therefore, to filter out value properties, we use the following hack:
// checker.getPropertiesOfType returns all exports that were originally
// values, but checker.getPropertyOfType returns undefined for
// properties that are mentioned in the typeOnlyExportStarMap.
const isThereAnyExportedValue = checker
.getPropertiesOfType(sourceFileType)
.some(
propertyTypeSymbol =>
checker.getPropertyOfType(
sourceFileType,
propertyTypeSymbol.escapedName.toString(),
) != null,
);
if (isThereAnyExportedValue) {
return;
}
context.report({
node,
messageId: 'typeOverValue',
fix(fixer) {
const asteriskToken = nullThrows(
context.sourceCode.getFirstToken(
node,
token =>
token.type === AST_TOKEN_TYPES.Punctuator &&
token.value === '*',
),
NullThrowsReasons.MissingToken(
'asterisk',
'export all declaration',
),
);
return fixer.insertTextBefore(asteriskToken, 'type ');
},
});
},
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void {
// Coerce the source into a string for use as a lookup entry.
const source = getSourceFromExport(node) ?? 'undefined';
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const sourceExports = (sourceExportsMap[source] ||= {
reportValueExports: [],
source,
typeOnlyNamedExport: null,
valueOnlyNamedExport: null,
});
// Cache the first encountered exports for the package. We will need to come
// back to these later when fixing the problems.
if (node.exportKind === 'type') {
// The export is a type export
sourceExports.typeOnlyNamedExport ??= node;
} else {
// The export is a value export
sourceExports.valueOnlyNamedExport ??= node;
}
// Next for the current export, we will separate type/value specifiers.
const typeBasedSpecifiers: TSESTree.ExportSpecifier[] = [];
const inlineTypeSpecifiers: TSESTree.ExportSpecifier[] = [];
const valueSpecifiers: TSESTree.ExportSpecifier[] = [];
// Note: it is valid to export values as types. We will avoid reporting errors
// when this is encountered.
if (node.exportKind !== 'type') {
for (const specifier of node.specifiers) {
if (specifier.exportKind === 'type') {
inlineTypeSpecifiers.push(specifier);
continue;
}
const isTypeBased = isSymbolTypeBased(
services.getSymbolAtLocation(specifier.exported),
);
if (isTypeBased === true) {
typeBasedSpecifiers.push(specifier);
} else if (isTypeBased === false) {
// When isTypeBased is undefined, we should avoid reporting them.
valueSpecifiers.push(specifier);
}
}
}
if (
(node.exportKind === 'value' && typeBasedSpecifiers.length) ||
(node.exportKind === 'type' && valueSpecifiers.length)
) {
sourceExports.reportValueExports.push({
node,
inlineTypeSpecifiers,
typeBasedSpecifiers,
valueSpecifiers,
});
}
},
'Program:exit'(): void {
for (const sourceExports of Object.values(sourceExportsMap)) {
// If this export has no issues, move on.
if (sourceExports.reportValueExports.length === 0) {
continue;
}
for (const report of sourceExports.reportValueExports) {
if (report.valueSpecifiers.length === 0) {
// Export is all type-only with no type specifiers; convert the entire export to `export type`.
context.report({
node: report.node,
messageId: 'typeOverValue',
*fix(fixer) {
yield* fixExportInsertType(
fixer,
context.sourceCode,
report.node,
);
},
});
continue;
}
// We have both type and value violations.
const allExportNames = report.typeBasedSpecifiers.map(specifier =>
specifier.local.type === AST_NODE_TYPES.Identifier
? specifier.local.name
: specifier.local.value,
);
if (allExportNames.length === 1) {
const exportNames = allExportNames[0];
context.report({
node: report.node,
messageId: 'singleExportIsType',
data: { exportNames },
*fix(fixer) {
if (fixMixedExportsWithInlineTypeSpecifier) {
yield* fixAddTypeSpecifierToNamedExports(fixer, report);
} else {
yield* fixSeparateNamedExports(
fixer,
context.sourceCode,
report,
);
}
},
});
} else {
const exportNames = formatWordList(allExportNames);
context.report({
node: report.node,
messageId: 'multipleExportsAreTypes',
data: { exportNames },
*fix(fixer) {
if (fixMixedExportsWithInlineTypeSpecifier) {
yield* fixAddTypeSpecifierToNamedExports(fixer, report);
} else {
yield* fixSeparateNamedExports(
fixer,
context.sourceCode,
report,
);
}
},
});
}
}
}
},
};
}
fixExportInsertType(fixer: TSESLint.RuleFixer, sourceCode: Readonly<TSESLint.SourceCode>, node: TSESTree.ExportNamedDeclaration): IterableIterator<TSESLint.RuleFix>¶
Inserts "type" into an export.
Example:
export type { Foo } from 'foo'; ^^^^
Raw JSDoc
Calls:
nullThrows (from ../util)sourceCode.getFirstTokenNullThrowsReasons.MissingTokenfixer.insertTextAftersourceCode.getTokenAfterfixer.removeRange
Code
function* fixExportInsertType(
fixer: TSESLint.RuleFixer,
sourceCode: Readonly<TSESLint.SourceCode>,
node: TSESTree.ExportNamedDeclaration,
): IterableIterator<TSESLint.RuleFix> {
const exportToken = nullThrows(
sourceCode.getFirstToken(node),
NullThrowsReasons.MissingToken('export', node.type),
);
yield fixer.insertTextAfter(exportToken, ' type');
for (const specifier of node.specifiers) {
if (specifier.exportKind === 'type') {
const kindToken = nullThrows(
sourceCode.getFirstToken(specifier),
NullThrowsReasons.MissingToken('export', specifier.type),
);
const firstTokenAfter = nullThrows(
sourceCode.getTokenAfter(kindToken, {
includeComments: true,
}),
'Missing token following the export kind.',
);
yield fixer.removeRange([kindToken.range[0], firstTokenAfter.range[0]]);
}
}
}
fixSeparateNamedExports(fixer: TSESLint.RuleFixer, sourceCode: Readonly<TSESLint.SourceCode>, report: ReportValueExport): IterableIterator<TSESLint.RuleFix>¶
Separates the exports which mismatch the kind of export the given
node represents. For example, a type export's named specifiers which
represent values will be inserted in a separate export statement.
Raw JSDoc
Calls:
getSourceFromExporttypeSpecifiers.map(getSpecifierText).joinnullThrows (from ../util)sourceCode.getFirstTokenNullThrowsReasons.MissingTokenvalueSpecifiers .map(getSpecifierText) .joinsourceCode.getLastTokenfixer.replaceTextRangefixer.insertTextBefore
Internal Comments:
// Filter the bad exports from the current line. (x2)
// Remove exports from the current line which we're going to re-insert. (x2)
// Insert the bad exports into a new export line above. (x2)
Code
function* fixSeparateNamedExports(
fixer: TSESLint.RuleFixer,
sourceCode: Readonly<TSESLint.SourceCode>,
report: ReportValueExport,
): IterableIterator<TSESLint.RuleFix> {
const { node, inlineTypeSpecifiers, typeBasedSpecifiers, valueSpecifiers } =
report;
const typeSpecifiers = [...typeBasedSpecifiers, ...inlineTypeSpecifiers];
const source = getSourceFromExport(node);
const specifierNames = typeSpecifiers.map(getSpecifierText).join(', ');
const exportToken = nullThrows(
sourceCode.getFirstToken(node),
NullThrowsReasons.MissingToken('export', node.type),
);
// Filter the bad exports from the current line.
const filteredSpecifierNames = valueSpecifiers
.map(getSpecifierText)
.join(', ');
const openToken = nullThrows(
sourceCode.getFirstToken(node, isOpeningBraceToken),
NullThrowsReasons.MissingToken('{', node.type),
);
const closeToken = nullThrows(
sourceCode.getLastToken(node, isClosingBraceToken),
NullThrowsReasons.MissingToken('}', node.type),
);
// Remove exports from the current line which we're going to re-insert.
yield fixer.replaceTextRange(
[openToken.range[1], closeToken.range[0]],
` ${filteredSpecifierNames} `,
);
// Insert the bad exports into a new export line above.
yield fixer.insertTextBefore(
exportToken,
`export type { ${specifierNames} }${source ? ` from '${source}'` : ''};\n`,
);
}
fixAddTypeSpecifierToNamedExports(fixer: TSESLint.RuleFixer, report: ReportValueExport): IterableIterator<TSESLint.RuleFix>¶
Parameters:
fixerTSESLint.RuleFixerreportReportValueExport
Returns: IterableIterator<TSESLint.RuleFix>
Calls:
fixer.insertTextBefore
Code
getSourceFromExport(node: TSESTree.ExportNamedDeclaration): string | undefined¶
Returns the source of the export, or undefined if the named export has no source.
Raw JSDoc
Code
getSpecifierText(specifier: TSESTree.ExportSpecifier): string¶
Returns the specifier text for the export. If it is aliased, we take care to return the proper formatting.
Raw JSDoc
Code
function getSpecifierText(specifier: TSESTree.ExportSpecifier): string {
const exportedName =
specifier.exported.type === AST_NODE_TYPES.Literal
? specifier.exported.raw
: specifier.exported.name;
const localName =
specifier.local.type === AST_NODE_TYPES.Literal
? specifier.local.raw
: specifier.local.name;
return `${localName}${
exportedName !== localName ? ` as ${exportedName}` : ''
}`;
}
Internal helpers¶
Declared inside another function in this file.
isSymbolTypeBased(symbol: ts.Symbol | undefined): boolean | undefined¶
Helper for identifying if a symbol resolves to a JavaScript value or a TypeScript type.
Returns: undefined
True/false if is a type or not, or undefined if the specifier
can't be resolved.
Raw JSDoc
Calls:
checker.isUnknownSymbolsymbol.getDeclarations()?.sometsutils.isSymbolFlagSetisSymbolTypeBasedchecker.getImmediateAliasedSymbol
Code
function isSymbolTypeBased(
symbol: ts.Symbol | undefined,
): boolean | undefined {
if (!symbol || checker.isUnknownSymbol(symbol)) {
return undefined;
}
if (
symbol.getDeclarations()?.some(ts.isTypeOnlyImportOrExportDeclaration)
) {
return true;
}
if (tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Value)) {
return false;
}
return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias)
? isSymbolTypeBased(checker.getImmediateAliasedSymbol(symbol))
: true;
}
Interfaces¶
SourceExports¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
reportValueExports |
ReportValueExport[] |
✗ | not shown |
source |
string |
✗ | not shown |
typeOnlyNamedExport |
TSESTree.ExportNamedDeclaration \| null |
✗ | not shown |
valueOnlyNamedExport |
TSESTree.ExportNamedDeclaration \| null |
✗ | not shown |
ReportValueExport¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
inlineTypeSpecifiers |
TSESTree.ExportSpecifier[] |
✗ | not shown |
node |
TSESTree.ExportNamedDeclaration |
✗ | not shown |
typeBasedSpecifiers |
TSESTree.ExportSpecifier[] |
✗ | not shown |
valueSpecifiers |
TSESTree.ExportSpecifier[] |
✗ | not shown |
Type Aliases¶
Options¶
MessageIds¶
Generated by Syntax Scribe