From 968c19cfb549e9bb2ac92b16ce00259e8ee31005 Mon Sep 17 00:00:00 2001 From: Sam Adams Date: Wed, 10 Dec 2025 08:29:04 -0600 Subject: [PATCH 1/2] Allow const JSON imports with type narrowing --- src/compiler/checker.ts | 68 ++++- .../reference/importAttributesConstJson.js | 134 ++++++++++ .../importAttributesConstJson.symbols | 143 +++++++++++ .../reference/importAttributesConstJson.types | 241 ++++++++++++++++++ .../compiler/jsonImportConstAttribute.ts | 38 +++ .../importAttributesConstJson.ts | 72 ++++++ 6 files changed, 690 insertions(+), 6 deletions(-) create mode 100644 tests/baselines/reference/importAttributesConstJson.js create mode 100644 tests/baselines/reference/importAttributesConstJson.symbols create mode 100644 tests/baselines/reference/importAttributesConstJson.types create mode 100644 tests/cases/compiler/jsonImportConstAttribute.ts create mode 100644 tests/cases/conformance/importAttributes/importAttributesConstJson.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ebd026658ff95..23ef1551b7693 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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 | undefined; @@ -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(symbol.declarations, isAliasSymbolDeclaration); } @@ -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); @@ -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)) || diff --git a/tests/baselines/reference/importAttributesConstJson.js b/tests/baselines/reference/importAttributesConstJson.js new file mode 100644 index 0000000000000..95a6209a7c26d --- /dev/null +++ b/tests/baselines/reference/importAttributesConstJson.js @@ -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 {}; diff --git a/tests/baselines/reference/importAttributesConstJson.symbols b/tests/baselines/reference/importAttributesConstJson.symbols new file mode 100644 index 0000000000000..811ebb78faa5b --- /dev/null +++ b/tests/baselines/reference/importAttributesConstJson.symbols @@ -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) + + diff --git a/tests/baselines/reference/importAttributesConstJson.types b/tests/baselines/reference/importAttributesConstJson.types new file mode 100644 index 0000000000000..495f76be3bb0f --- /dev/null +++ b/tests/baselines/reference/importAttributesConstJson.types @@ -0,0 +1,241 @@ +//// [tests/cases/conformance/importAttributes/importAttributesConstJson.ts] //// + +=== config.json === +{ +>{ "appLocales": ["FR", "BE"], "debug": true, "count": 42, "settings": { "theme": "dark", "notifications": false }} : { appLocales: string[]; debug: boolean; count: number; settings: { theme: string; notifications: boolean; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + "appLocales": ["FR", "BE"], +>"appLocales" : string[] +> : ^^^^^^^^ +>["FR", "BE"] : string[] +> : ^^^^^^^^ +>"FR" : "FR" +> : ^^^^ +>"BE" : "BE" +> : ^^^^ + + "debug": true, +>"debug" : boolean +> : ^^^^^^^ +>true : true +> : ^^^^ + + "count": 42, +>"count" : number +> : ^^^^^^ +>42 : 42 +> : ^^ + + "settings": { +>"settings" : { theme: string; notifications: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ "theme": "dark", "notifications": false } : { theme: string; notifications: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + "theme": "dark", +>"theme" : string +> : ^^^^^^ +>"dark" : "dark" +> : ^^^^^^ + + "notifications": false +>"notifications" : boolean +> : ^^^^^^^ +>false : false +> : ^^^^^ + } +} + +=== without-const.ts === +// Without const attribute, types should be widened +import data from "./config.json" with { type: "json" }; +>data : { appLocales: string[]; debug: boolean; count: number; settings: { theme: string; notifications: boolean; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : error + +// Should be string[], not ("FR" | "BE")[] +export const locales = data.appLocales; +>locales : string[] +> : ^^^^^^^^ +>data.appLocales : string[] +> : ^^^^^^^^ +>data : { appLocales: string[]; debug: boolean; count: number; settings: { theme: string; notifications: boolean; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : string[] +> : ^^^^^^^^ + +// Should be boolean, not true +export const debug = data.debug; +>debug : boolean +> : ^^^^^^^ +>data.debug : boolean +> : ^^^^^^^ +>data : { appLocales: string[]; debug: boolean; count: number; settings: { theme: string; notifications: boolean; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>debug : boolean +> : ^^^^^^^ + +// Should be number, not 42 +export const count = data.count; +>count : number +> : ^^^^^^ +>data.count : number +> : ^^^^^^ +>data : { appLocales: string[]; debug: boolean; count: number; settings: { theme: string; notifications: boolean; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>count : number +> : ^^^^^^ + +// Should be string, not "dark" +export const theme = data.settings.theme; +>theme : string +> : ^^^^^^ +>data.settings.theme : string +> : ^^^^^^ +>data.settings : { theme: string; notifications: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>data : { appLocales: string[]; debug: boolean; count: number; settings: { theme: string; notifications: boolean; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>settings : { theme: string; notifications: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>theme : string +> : ^^^^^^ + +=== with-const.ts === +// With const: "true" attribute, types should preserve literal types +import data from "./config.json" with { type: "json", const: "true" }; +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : error +>const : error + +// Should be readonly ["FR", "BE"], not string[] +export const locales = data.appLocales; +>locales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>data.appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ + +// Should be true, not boolean +export const debug = data.debug; +>debug : true +> : ^^^^ +>data.debug : true +> : ^^^^ +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>debug : true +> : ^^^^ + +// Should be 42, not number +export const count = data.count; +>count : 42 +> : ^^ +>data.count : 42 +> : ^^ +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>count : 42 +> : ^^ + +// Should be "dark", not string +export const theme = data.settings.theme; +>theme : "dark" +> : ^^^^^^ +>data.settings.theme : "dark" +> : ^^^^^^ +>data.settings : { readonly theme: "dark"; readonly notifications: false; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>settings : { readonly theme: "dark"; readonly notifications: false; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>theme : "dark" +> : ^^^^^^ + +=== with-const-namespace.ts === +// Test namespace import with const: "true" +import * as ns from "./config.json" with { type: "json", const: "true" }; +>ns : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : error +>const : error + +// Should preserve literal types through namespace import +export const locales = ns.appLocales; +>locales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>ns.appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>ns : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ + +export const debug = ns.debug; +>debug : true +> : ^^^^ +>ns.debug : true +> : ^^^^ +>ns : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>debug : true +> : ^^^^ + +=== 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 : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : error +>const : error + +type AppLocale = "FR" | "BE"; +>AppLocale : AppLocale +> : ^^^^^^^^^ + +// This should work - appLocales should be a tuple of literal types +const locale: AppLocale = data.appLocales[0]; +>locale : AppLocale +> : ^^^^^^^^^ +>data.appLocales[0] : "FR" +> : ^^^^ +>data.appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>0 : 0 +> : ^ + +// Function that requires specific literal types +function setLocale(locale: "FR" | "BE"): void {} +>setLocale : (locale: "FR" | "BE") => void +> : ^ ^^ ^^^^^ +>locale : "FR" | "BE" +> : ^^^^^^^^^^^ + +// This should work with const import +setLocale(data.appLocales[0]); +>setLocale(data.appLocales[0]) : void +> : ^^^^ +>setLocale : (locale: "FR" | "BE") => void +> : ^ ^^ ^^^^^ +>data.appLocales[0] : "FR" +> : ^^^^ +>data.appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>data : { readonly appLocales: readonly ["FR", "BE"]; readonly debug: true; readonly count: 42; readonly settings: { readonly theme: "dark"; readonly notifications: false; }; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>0 : 0 +> : ^ + + diff --git a/tests/cases/compiler/jsonImportConstAttribute.ts b/tests/cases/compiler/jsonImportConstAttribute.ts new file mode 100644 index 0000000000000..47720b686c4e0 --- /dev/null +++ b/tests/cases/compiler/jsonImportConstAttribute.ts @@ -0,0 +1,38 @@ +// @module: esnext +// @moduleResolution: bundler +// @resolveJsonModule: true +// @strict: true +// @noEmit: true + +// @filename: /config.json +{ + "appLocales": ["FR", "BE"], + "enabled": true, + "count": 42 +} + +// @filename: /main.ts +// Without const attribute - types should be widened +import config from "./config.json" with { type: "json" }; + +// With const attribute - types should preserve literal types +import constConfig from "./config.json" with { type: "json", const: "true" }; + +// Test widened types (without const) +type Locales = typeof config.appLocales; // Should be string[] +type Enabled = typeof config.enabled; // Should be boolean +type Count = typeof config.count; // Should be number + +// Test literal types (with const) +type ConstLocales = typeof constConfig.appLocales; // Should be readonly ["FR", "BE"] +type ConstEnabled = typeof constConfig.enabled; // Should be true +type ConstCount = typeof constConfig.count; // Should be 42 + +// Verify the types work as expected +const locale: "FR" | "BE" = constConfig.appLocales[0]; // Should work with const +const enabledTrue: true = constConfig.enabled; // Should work with const + +// These should error without const (uncomment to verify errors) +// const localeError: "FR" | "BE" = config.appLocales[0]; // Error: string not assignable to "FR" | "BE" +// const enabledError: true = config.enabled; // Error: boolean not assignable to true + diff --git a/tests/cases/conformance/importAttributes/importAttributesConstJson.ts b/tests/cases/conformance/importAttributes/importAttributesConstJson.ts new file mode 100644 index 0000000000000..437573906df2f --- /dev/null +++ b/tests/cases/conformance/importAttributes/importAttributesConstJson.ts @@ -0,0 +1,72 @@ +// @module: esnext +// @resolveJsonModule: true +// @target: esnext +// @declaration: true +// @outDir: ./out + +// @filename: config.json +{ + "appLocales": ["FR", "BE"], + "debug": true, + "count": 42, + "settings": { + "theme": "dark", + "notifications": false + } +} + +// @filename: 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; + +// @filename: 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; + +// @filename: 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; + +// @filename: 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]); + From 02d3dfb8d390839006eac33021ebb73c73a53452 Mon Sep 17 00:00:00 2001 From: Sam Adams Date: Wed, 10 Dec 2025 09:03:00 -0600 Subject: [PATCH 2/2] Add missing baseline files for jsonImportConstAttribute test --- .../jsonImportConstAttribute.symbols | 80 ++++++++++ .../reference/jsonImportConstAttribute.types | 138 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 tests/baselines/reference/jsonImportConstAttribute.symbols create mode 100644 tests/baselines/reference/jsonImportConstAttribute.types diff --git a/tests/baselines/reference/jsonImportConstAttribute.symbols b/tests/baselines/reference/jsonImportConstAttribute.symbols new file mode 100644 index 0000000000000..7e15ce773410a --- /dev/null +++ b/tests/baselines/reference/jsonImportConstAttribute.symbols @@ -0,0 +1,80 @@ +//// [tests/cases/compiler/jsonImportConstAttribute.ts] //// + +=== /config.json === +{ + "appLocales": ["FR", "BE"], +>"appLocales" : Symbol("appLocales", Decl(config.json, 0, 1)) + + "enabled": true, +>"enabled" : Symbol("enabled", Decl(config.json, 1, 31)) + + "count": 42 +>"count" : Symbol("count", Decl(config.json, 2, 20)) +} + +=== /main.ts === +// Without const attribute - types should be widened +import config from "./config.json" with { type: "json" }; +>config : Symbol(config, Decl(main.ts, 1, 6)) + +// With const attribute - types should preserve literal types +import constConfig from "./config.json" with { type: "json", const: "true" }; +>constConfig : Symbol(constConfig, Decl(main.ts, 4, 6)) + +// Test widened types (without const) +type Locales = typeof config.appLocales; // Should be string[] +>Locales : Symbol(Locales, Decl(main.ts, 4, 77)) +>config.appLocales : Symbol("appLocales", Decl(config.json, 0, 1)) +>config : Symbol(config, Decl(main.ts, 1, 6)) +>appLocales : Symbol("appLocales", Decl(config.json, 0, 1)) + +type Enabled = typeof config.enabled; // Should be boolean +>Enabled : Symbol(Enabled, Decl(main.ts, 7, 40)) +>config.enabled : Symbol("enabled", Decl(config.json, 1, 31)) +>config : Symbol(config, Decl(main.ts, 1, 6)) +>enabled : Symbol("enabled", Decl(config.json, 1, 31)) + +type Count = typeof config.count; // Should be number +>Count : Symbol(Count, Decl(main.ts, 8, 37)) +>config.count : Symbol("count", Decl(config.json, 2, 20)) +>config : Symbol(config, Decl(main.ts, 1, 6)) +>count : Symbol("count", Decl(config.json, 2, 20)) + +// Test literal types (with const) +type ConstLocales = typeof constConfig.appLocales; // Should be readonly ["FR", "BE"] +>ConstLocales : Symbol(ConstLocales, Decl(main.ts, 9, 33)) +>constConfig.appLocales : Symbol("appLocales", Decl(config.json, 0, 1)) +>constConfig : Symbol(constConfig, Decl(main.ts, 4, 6)) +>appLocales : Symbol("appLocales", Decl(config.json, 0, 1)) + +type ConstEnabled = typeof constConfig.enabled; // Should be true +>ConstEnabled : Symbol(ConstEnabled, Decl(main.ts, 12, 50)) +>constConfig.enabled : Symbol("enabled", Decl(config.json, 1, 31)) +>constConfig : Symbol(constConfig, Decl(main.ts, 4, 6)) +>enabled : Symbol("enabled", Decl(config.json, 1, 31)) + +type ConstCount = typeof constConfig.count; // Should be 42 +>ConstCount : Symbol(ConstCount, Decl(main.ts, 13, 47)) +>constConfig.count : Symbol("count", Decl(config.json, 2, 20)) +>constConfig : Symbol(constConfig, Decl(main.ts, 4, 6)) +>count : Symbol("count", Decl(config.json, 2, 20)) + +// Verify the types work as expected +const locale: "FR" | "BE" = constConfig.appLocales[0]; // Should work with const +>locale : Symbol(locale, Decl(main.ts, 17, 5)) +>constConfig.appLocales : Symbol("appLocales", Decl(config.json, 0, 1)) +>constConfig : Symbol(constConfig, Decl(main.ts, 4, 6)) +>appLocales : Symbol("appLocales", Decl(config.json, 0, 1)) +>0 : Symbol(0) + +const enabledTrue: true = constConfig.enabled; // Should work with const +>enabledTrue : Symbol(enabledTrue, Decl(main.ts, 18, 5)) +>constConfig.enabled : Symbol("enabled", Decl(config.json, 1, 31)) +>constConfig : Symbol(constConfig, Decl(main.ts, 4, 6)) +>enabled : Symbol("enabled", Decl(config.json, 1, 31)) + +// These should error without const (uncomment to verify errors) +// const localeError: "FR" | "BE" = config.appLocales[0]; // Error: string not assignable to "FR" | "BE" +// const enabledError: true = config.enabled; // Error: boolean not assignable to true + + diff --git a/tests/baselines/reference/jsonImportConstAttribute.types b/tests/baselines/reference/jsonImportConstAttribute.types new file mode 100644 index 0000000000000..c44253a163d94 --- /dev/null +++ b/tests/baselines/reference/jsonImportConstAttribute.types @@ -0,0 +1,138 @@ +//// [tests/cases/compiler/jsonImportConstAttribute.ts] //// + +=== /config.json === +{ +>{ "appLocales": ["FR", "BE"], "enabled": true, "count": 42} : { appLocales: string[]; enabled: boolean; count: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + "appLocales": ["FR", "BE"], +>"appLocales" : string[] +> : ^^^^^^^^ +>["FR", "BE"] : string[] +> : ^^^^^^^^ +>"FR" : "FR" +> : ^^^^ +>"BE" : "BE" +> : ^^^^ + + "enabled": true, +>"enabled" : boolean +> : ^^^^^^^ +>true : true +> : ^^^^ + + "count": 42 +>"count" : number +> : ^^^^^^ +>42 : 42 +> : ^^ +} + +=== /main.ts === +// Without const attribute - types should be widened +import config from "./config.json" with { type: "json" }; +>config : { appLocales: string[]; enabled: boolean; count: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : error + +// With const attribute - types should preserve literal types +import constConfig from "./config.json" with { type: "json", const: "true" }; +>constConfig : { readonly appLocales: readonly ["FR", "BE"]; readonly enabled: true; readonly count: 42; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : error +>const : error + +// Test widened types (without const) +type Locales = typeof config.appLocales; // Should be string[] +>Locales : string[] +> : ^^^^^^^^ +>config.appLocales : string[] +> : ^^^^^^^^ +>config : { appLocales: string[]; enabled: boolean; count: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : string[] +> : ^^^^^^^^ + +type Enabled = typeof config.enabled; // Should be boolean +>Enabled : boolean +> : ^^^^^^^ +>config.enabled : boolean +> : ^^^^^^^ +>config : { appLocales: string[]; enabled: boolean; count: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>enabled : boolean +> : ^^^^^^^ + +type Count = typeof config.count; // Should be number +>Count : number +> : ^^^^^^ +>config.count : number +> : ^^^^^^ +>config : { appLocales: string[]; enabled: boolean; count: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>count : number +> : ^^^^^^ + +// Test literal types (with const) +type ConstLocales = typeof constConfig.appLocales; // Should be readonly ["FR", "BE"] +>ConstLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>constConfig.appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>constConfig : { readonly appLocales: readonly ["FR", "BE"]; readonly enabled: true; readonly count: 42; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ + +type ConstEnabled = typeof constConfig.enabled; // Should be true +>ConstEnabled : true +> : ^^^^ +>constConfig.enabled : true +> : ^^^^ +>constConfig : { readonly appLocales: readonly ["FR", "BE"]; readonly enabled: true; readonly count: 42; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>enabled : true +> : ^^^^ + +type ConstCount = typeof constConfig.count; // Should be 42 +>ConstCount : 42 +> : ^^ +>constConfig.count : 42 +> : ^^ +>constConfig : { readonly appLocales: readonly ["FR", "BE"]; readonly enabled: true; readonly count: 42; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>count : 42 +> : ^^ + +// Verify the types work as expected +const locale: "FR" | "BE" = constConfig.appLocales[0]; // Should work with const +>locale : "FR" | "BE" +> : ^^^^^^^^^^^ +>constConfig.appLocales[0] : "FR" +> : ^^^^ +>constConfig.appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>constConfig : { readonly appLocales: readonly ["FR", "BE"]; readonly enabled: true; readonly count: 42; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>appLocales : readonly ["FR", "BE"] +> : ^^^^^^^^^^^^^^^^^^^^^ +>0 : 0 +> : ^ + +const enabledTrue: true = constConfig.enabled; // Should work with const +>enabledTrue : true +> : ^^^^ +>true : true +> : ^^^^ +>constConfig.enabled : true +> : ^^^^ +>constConfig : { readonly appLocales: readonly ["FR", "BE"]; readonly enabled: true; readonly count: 42; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>enabled : true +> : ^^^^ + +// These should error without const (uncomment to verify errors) +// const localeError: "FR" | "BE" = config.appLocales[0]; // Error: string not assignable to "FR" | "BE" +// const enabledError: true = config.enabled; // Error: boolean not assignable to true + +