Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 62 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var currentNode: Node | undefined;
var varianceTypeParameter: TypeParameter | undefined;
var isInferencePartiallyBlocked = false;
var inConstImportContext = false;
var withinUnreachableCode = false;
var reportedUnreachableNodes: Set<Node> | undefined;

Expand Down Expand Up @@ -3654,6 +3655,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

/**
* Returns true if the import or export declaration has a `const: true` attribute.
* This is used for JSON modules to preserve literal types instead of widening them.
* Example: import data from "./data.json" with { type: "json", const: true };
*/
function hasConstImportAttribute(node: ImportDeclaration | ExportDeclaration | JSDocImportTag): boolean {
const attributes = node.attributes;
if (!attributes) {
return false;
}
for (const attr of attributes.elements) {
const name = getNameFromImportAttribute(attr);
if (name === "const") {
// Support both `const: true` (boolean literal) and `const: "true"` (string literal)
if (attr.value.kind === SyntaxKind.TrueKeyword) {
return true;
}
if (isStringLiteral(attr.value) && attr.value.text === "true") {
return true;
}
}
}
return false;
}

function getDeclarationOfAliasSymbol(symbol: Symbol): Declaration | undefined {
return symbol.declarations && findLast<Declaration>(symbol.declarations, isAliasSymbolDeclaration);
}
Expand Down Expand Up @@ -12783,19 +12809,45 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return errorType;
}
const targetSymbol = resolveAlias(symbol);
const exportSymbol = symbol.declarations && getTargetOfAliasDeclaration(getDeclarationOfAliasSymbol(symbol)!, /*dontRecursivelyResolve*/ true);
const aliasDeclaration = symbol.declarations && getDeclarationOfAliasSymbol(symbol);
const exportSymbol = aliasDeclaration && getTargetOfAliasDeclaration(aliasDeclaration, /*dontRecursivelyResolve*/ true);
const declaredType = firstDefined(exportSymbol?.declarations, d => isExportAssignment(d) ? tryGetTypeFromEffectiveTypeNode(d) : undefined);

// Check if this is a JSON import with const: true attribute
const importDeclaration = aliasDeclaration && getAnyImportSyntax(aliasDeclaration);
const hasConstAttribute = importDeclaration && (isImportDeclaration(importDeclaration) || isJSDocImportTag(importDeclaration)) && hasConstImportAttribute(importDeclaration);
const targetFile = targetSymbol && getSourceFileOfModule(targetSymbol);
const isJsonModule = targetFile && isJsonSourceFile(targetFile);

// It only makes sense to get the type of a value symbol. If the result of resolving
// the alias is not a value, then it has no type. To get the type associated with a
// type symbol, call getDeclaredTypeOfSymbol.
// This check is important because without it, a call to getTypeOfSymbol could end
// up recursively calling getTypeOfAlias, causing a stack overflow.
links.type ??= exportSymbol?.declarations && isDuplicatedCommonJSExport(exportSymbol.declarations) && symbol.declarations!.length ? getFlowTypeFromCommonJSExport(exportSymbol)
: isDuplicatedCommonJSExport(symbol.declarations) ? autoType
: declaredType ? declaredType
: getSymbolFlags(targetSymbol) & SymbolFlags.Value ? getTypeOfSymbol(targetSymbol)
: errorType;
if (exportSymbol?.declarations && isDuplicatedCommonJSExport(exportSymbol.declarations) && symbol.declarations!.length) {
links.type ??= getFlowTypeFromCommonJSExport(exportSymbol);
}
else if (isDuplicatedCommonJSExport(symbol.declarations)) {
links.type ??= autoType;
}
else if (declaredType) {
links.type ??= declaredType;
}
else if (getSymbolFlags(targetSymbol) & SymbolFlags.Value) {
// For JSON modules with const: true attribute, use non-widened literal types
if (hasConstAttribute && isJsonModule && targetFile.statements.length) {
const savedInConstImportContext = inConstImportContext;
inConstImportContext = true;
links.type ??= getRegularTypeOfLiteralType(checkExpression(targetFile.statements[0].expression));
inConstImportContext = savedInConstImportContext;
}
else {
links.type ??= getTypeOfSymbol(targetSymbol);
}
}
else {
links.type ??= errorType;
}

if (!popTypeResolution()) {
reportCircularityError(exportSymbol ?? symbol);
Expand Down Expand Up @@ -41414,6 +41466,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function isConstContext(node: Expression): boolean {
const parent = node.parent;
// Check if we're in a const import context (e.g., import from "./data.json" with { const: "true" })
if (inConstImportContext) {
return true;
}
return isAssertionExpression(parent) && isConstTypeReference(parent.type) ||
isJSDocTypeAssertion(parent) && isConstTypeReference(getJSDocTypeAssertionType(parent)) ||
isValidConstAssertionArgument(node) && isConstTypeVariable(getContextualType(node, ContextFlags.None)) ||
Expand Down
134 changes: 134 additions & 0 deletions tests/baselines/reference/importAttributesConstJson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//// [tests/cases/conformance/importAttributes/importAttributesConstJson.ts] ////

//// [config.json]
{
"appLocales": ["FR", "BE"],
"debug": true,
"count": 42,
"settings": {
"theme": "dark",
"notifications": false
}
}

//// [without-const.ts]
// Without const attribute, types should be widened
import data from "./config.json" with { type: "json" };

// Should be string[], not ("FR" | "BE")[]
export const locales = data.appLocales;

// Should be boolean, not true
export const debug = data.debug;

// Should be number, not 42
export const count = data.count;

// Should be string, not "dark"
export const theme = data.settings.theme;

//// [with-const.ts]
// With const: "true" attribute, types should preserve literal types
import data from "./config.json" with { type: "json", const: "true" };

// Should be readonly ["FR", "BE"], not string[]
export const locales = data.appLocales;

// Should be true, not boolean
export const debug = data.debug;

// Should be 42, not number
export const count = data.count;

// Should be "dark", not string
export const theme = data.settings.theme;

//// [with-const-namespace.ts]
// Test namespace import with const: "true"
import * as ns from "./config.json" with { type: "json", const: "true" };

// Should preserve literal types through namespace import
export const locales = ns.appLocales;
export const debug = ns.debug;

//// [type-assertions.ts]
// Test that const attribute produces types compatible with literal type assertions
import data from "./config.json" with { type: "json", const: "true" };

type AppLocale = "FR" | "BE";

// This should work - appLocales should be a tuple of literal types
const locale: AppLocale = data.appLocales[0];

// Function that requires specific literal types
function setLocale(locale: "FR" | "BE"): void {}

// This should work with const import
setLocale(data.appLocales[0]);



//// [config.json]
{
"appLocales": ["FR", "BE"],
"debug": true,
"count": 42,
"settings": {
"theme": "dark",
"notifications": false
}
}
//// [without-const.js]
// Without const attribute, types should be widened
import data from "./config.json" with { type: "json" };
// Should be string[], not ("FR" | "BE")[]
export const locales = data.appLocales;
// Should be boolean, not true
export const debug = data.debug;
// Should be number, not 42
export const count = data.count;
// Should be string, not "dark"
export const theme = data.settings.theme;
//// [with-const.js]
// With const: "true" attribute, types should preserve literal types
import data from "./config.json" with { type: "json", const: "true" };
// Should be readonly ["FR", "BE"], not string[]
export const locales = data.appLocales;
// Should be true, not boolean
export const debug = data.debug;
// Should be 42, not number
export const count = data.count;
// Should be "dark", not string
export const theme = data.settings.theme;
//// [with-const-namespace.js]
// Test namespace import with const: "true"
import * as ns from "./config.json" with { type: "json", const: "true" };
// Should preserve literal types through namespace import
export const locales = ns.appLocales;
export const debug = ns.debug;
//// [type-assertions.js]
// Test that const attribute produces types compatible with literal type assertions
import data from "./config.json" with { type: "json", const: "true" };
// This should work - appLocales should be a tuple of literal types
const locale = data.appLocales[0];
// Function that requires specific literal types
function setLocale(locale) { }
// This should work with const import
setLocale(data.appLocales[0]);


//// [without-const.d.ts]
export declare const locales: string[];
export declare const debug: boolean;
export declare const count: number;
export declare const theme: string;
//// [with-const.d.ts]
export declare const locales: readonly ["FR", "BE"];
export declare const debug: true;
export declare const count: 42;
export declare const theme: "dark";
//// [with-const-namespace.d.ts]
export declare const locales: readonly ["FR", "BE"];
export declare const debug: true;
//// [type-assertions.d.ts]
export {};
143 changes: 143 additions & 0 deletions tests/baselines/reference/importAttributesConstJson.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//// [tests/cases/conformance/importAttributes/importAttributesConstJson.ts] ////

=== config.json ===
{
"appLocales": ["FR", "BE"],
>"appLocales" : Symbol("appLocales", Decl(config.json, 0, 1))

"debug": true,
>"debug" : Symbol("debug", Decl(config.json, 1, 31))

"count": 42,
>"count" : Symbol("count", Decl(config.json, 2, 18))

"settings": {
>"settings" : Symbol("settings", Decl(config.json, 3, 16))

"theme": "dark",
>"theme" : Symbol("theme", Decl(config.json, 4, 17))

"notifications": false
>"notifications" : Symbol("notifications", Decl(config.json, 5, 24))
}
}

=== without-const.ts ===
// Without const attribute, types should be widened
import data from "./config.json" with { type: "json" };
>data : Symbol(data, Decl(without-const.ts, 1, 6))

// Should be string[], not ("FR" | "BE")[]
export const locales = data.appLocales;
>locales : Symbol(locales, Decl(without-const.ts, 4, 12))
>data.appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>data : Symbol(data, Decl(without-const.ts, 1, 6))
>appLocales : Symbol("appLocales", Decl(config.json, 0, 1))

// Should be boolean, not true
export const debug = data.debug;
>debug : Symbol(debug, Decl(without-const.ts, 7, 12))
>data.debug : Symbol("debug", Decl(config.json, 1, 31))
>data : Symbol(data, Decl(without-const.ts, 1, 6))
>debug : Symbol("debug", Decl(config.json, 1, 31))

// Should be number, not 42
export const count = data.count;
>count : Symbol(count, Decl(without-const.ts, 10, 12))
>data.count : Symbol("count", Decl(config.json, 2, 18))
>data : Symbol(data, Decl(without-const.ts, 1, 6))
>count : Symbol("count", Decl(config.json, 2, 18))

// Should be string, not "dark"
export const theme = data.settings.theme;
>theme : Symbol(theme, Decl(without-const.ts, 13, 12))
>data.settings.theme : Symbol("theme", Decl(config.json, 4, 17))
>data.settings : Symbol("settings", Decl(config.json, 3, 16))
>data : Symbol(data, Decl(without-const.ts, 1, 6))
>settings : Symbol("settings", Decl(config.json, 3, 16))
>theme : Symbol("theme", Decl(config.json, 4, 17))

=== with-const.ts ===
// With const: "true" attribute, types should preserve literal types
import data from "./config.json" with { type: "json", const: "true" };
>data : Symbol(data, Decl(with-const.ts, 1, 6))

// Should be readonly ["FR", "BE"], not string[]
export const locales = data.appLocales;
>locales : Symbol(locales, Decl(with-const.ts, 4, 12))
>data.appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>data : Symbol(data, Decl(with-const.ts, 1, 6))
>appLocales : Symbol("appLocales", Decl(config.json, 0, 1))

// Should be true, not boolean
export const debug = data.debug;
>debug : Symbol(debug, Decl(with-const.ts, 7, 12))
>data.debug : Symbol("debug", Decl(config.json, 1, 31))
>data : Symbol(data, Decl(with-const.ts, 1, 6))
>debug : Symbol("debug", Decl(config.json, 1, 31))

// Should be 42, not number
export const count = data.count;
>count : Symbol(count, Decl(with-const.ts, 10, 12))
>data.count : Symbol("count", Decl(config.json, 2, 18))
>data : Symbol(data, Decl(with-const.ts, 1, 6))
>count : Symbol("count", Decl(config.json, 2, 18))

// Should be "dark", not string
export const theme = data.settings.theme;
>theme : Symbol(theme, Decl(with-const.ts, 13, 12))
>data.settings.theme : Symbol("theme", Decl(config.json, 4, 17))
>data.settings : Symbol("settings", Decl(config.json, 3, 16))
>data : Symbol(data, Decl(with-const.ts, 1, 6))
>settings : Symbol("settings", Decl(config.json, 3, 16))
>theme : Symbol("theme", Decl(config.json, 4, 17))

=== with-const-namespace.ts ===
// Test namespace import with const: "true"
import * as ns from "./config.json" with { type: "json", const: "true" };
>ns : Symbol(ns, Decl(with-const-namespace.ts, 1, 6))

// Should preserve literal types through namespace import
export const locales = ns.appLocales;
>locales : Symbol(locales, Decl(with-const-namespace.ts, 4, 12))
>ns.appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>ns : Symbol(ns, Decl(with-const-namespace.ts, 1, 6))
>appLocales : Symbol("appLocales", Decl(config.json, 0, 1))

export const debug = ns.debug;
>debug : Symbol(debug, Decl(with-const-namespace.ts, 5, 12))
>ns.debug : Symbol("debug", Decl(config.json, 1, 31))
>ns : Symbol(ns, Decl(with-const-namespace.ts, 1, 6))
>debug : Symbol("debug", Decl(config.json, 1, 31))

=== type-assertions.ts ===
// Test that const attribute produces types compatible with literal type assertions
import data from "./config.json" with { type: "json", const: "true" };
>data : Symbol(data, Decl(type-assertions.ts, 1, 6))

type AppLocale = "FR" | "BE";
>AppLocale : Symbol(AppLocale, Decl(type-assertions.ts, 1, 70))

// This should work - appLocales should be a tuple of literal types
const locale: AppLocale = data.appLocales[0];
>locale : Symbol(locale, Decl(type-assertions.ts, 6, 5))
>AppLocale : Symbol(AppLocale, Decl(type-assertions.ts, 1, 70))
>data.appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>data : Symbol(data, Decl(type-assertions.ts, 1, 6))
>appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>0 : Symbol(0)

// Function that requires specific literal types
function setLocale(locale: "FR" | "BE"): void {}
>setLocale : Symbol(setLocale, Decl(type-assertions.ts, 6, 45))
>locale : Symbol(locale, Decl(type-assertions.ts, 9, 19))

// This should work with const import
setLocale(data.appLocales[0]);
>setLocale : Symbol(setLocale, Decl(type-assertions.ts, 6, 45))
>data.appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>data : Symbol(data, Decl(type-assertions.ts, 1, 6))
>appLocales : Symbol("appLocales", Decl(config.json, 0, 1))
>0 : Symbol(0)


Loading