Skip to content

Commit f1cc467

Browse files
committed
fix(typegen): preserve non-identifier keys in generated types
1 parent e6f4485 commit f1cc467

File tree

3 files changed

+60
-5
lines changed

3 files changed

+60
-5
lines changed

packages/@sanity/codegen/src/typescript/__tests__/schemaTypeGenerator.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ describe(SchemaTypeGenerator.name, () => {
4747
members: [
4848
{
4949
key: {name: '_id', type: 'Identifier'},
50+
optional: undefined,
5051
type: 'TSPropertySignature',
5152
typeAnnotation: {type: 'TSTypeAnnotation', typeAnnotation: {type: 'TSStringKeyword'}},
5253
},
5354
{
5455
key: {name: '_type', type: 'Identifier'},
56+
optional: undefined,
5557
type: 'TSPropertySignature',
5658
typeAnnotation: {
5759
type: 'TSTypeAnnotation',
@@ -360,6 +362,52 @@ describe(SchemaTypeGenerator.name, () => {
360362
)
361363
})
362364

365+
test('quotes non-identifier keys, preserves valid identifier keys', () => {
366+
const schema = new SchemaTypeGenerator([
367+
{
368+
type: 'type',
369+
name: 'objectWithMixedKeys',
370+
value: {
371+
type: 'object',
372+
attributes: {
373+
// Valid identifiers - should NOT be quoted
374+
'normalKey': {type: 'objectAttribute', value: {type: 'string'}},
375+
'_privateKey': {type: 'objectAttribute', value: {type: 'string'}},
376+
'$dollarKey': {type: 'objectAttribute', value: {type: 'string'}},
377+
'camelCase': {type: 'objectAttribute', value: {type: 'string'}},
378+
'PascalCase': {type: 'objectAttribute', value: {type: 'string'}},
379+
'UPPER_SNAKE': {type: 'objectAttribute', value: {type: 'string'}},
380+
// Invalid identifiers - MUST be quoted
381+
'kebab-case': {type: 'objectAttribute', value: {type: 'string'}},
382+
'dot.notation': {type: 'objectAttribute', value: {type: 'string'}},
383+
'with spaces': {type: 'objectAttribute', value: {type: 'string'}},
384+
'123startsWithNumber': {type: 'objectAttribute', value: {type: 'string'}},
385+
'special@char': {type: 'objectAttribute', value: {type: 'string'}},
386+
'': {type: 'objectAttribute', value: {type: 'string'}},
387+
},
388+
},
389+
},
390+
])
391+
392+
const objectType = schema.getType('objectWithMixedKeys')?.tsType
393+
expect(generateCode(objectType)).toMatchInlineSnapshot(`
394+
"{
395+
normalKey: string;
396+
_privateKey: string;
397+
$dollarKey: string;
398+
camelCase: string;
399+
PascalCase: string;
400+
UPPER_SNAKE: string;
401+
"kebab-case": string;
402+
"dot.notation": string;
403+
"with spaces": string;
404+
"123startsWithNumber": string;
405+
"special@char": string;
406+
"": string;
407+
}"
408+
`)
409+
})
410+
363411
test('generates TS Types for objects', () => {
364412
const schema = new SchemaTypeGenerator([
365413
{

packages/@sanity/codegen/src/typescript/helpers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export function sanitizeIdentifier(input: string): string {
1414
return `${input.replace(/^\d/, '_').replace(/[^$\w]+(.)/g, (_, char) => char.toUpperCase())}`
1515
}
1616

17+
/**
18+
* Checks if a string is a valid ECMAScript IdentifierName.
19+
* IdentifierNames start with a letter, underscore, or $, and contain only
20+
* alphanumeric characters, underscores, or $.
21+
*/
22+
export function isIdentifierName(input: string): boolean {
23+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(input)
24+
}
25+
1726
export function normalizeIdentifier(input: string): string {
1827
const sanitized = sanitizeIdentifier(input)
1928
return `${sanitized.charAt(0).toUpperCase()}${sanitized.slice(1)}`

packages/@sanity/codegen/src/typescript/schemaTypeGenerator.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414

1515
import {safeParseQuery} from '../safeParseQuery'
1616
import {INTERNAL_REFERENCE_SYMBOL} from './constants'
17-
import {getUniqueIdentifierForName, sanitizeIdentifier, weakMapMemo} from './helpers'
17+
import {getUniqueIdentifierForName, isIdentifierName, weakMapMemo} from './helpers'
1818
import {type ExtractedQuery, type TypeEvaluationStats} from './types'
1919

2020
export class SchemaTypeGenerator {
@@ -115,10 +115,8 @@ export class SchemaTypeGenerator {
115115
// Helper function used to generate TS types for object properties.
116116
private generateTsObjectProperty(key: string, attribute: ObjectAttribute): t.TSPropertySignature {
117117
const type = this.generateTsType(attribute.value)
118-
const propertySignature = t.tsPropertySignature(
119-
t.identifier(sanitizeIdentifier(key)),
120-
t.tsTypeAnnotation(type),
121-
)
118+
const keyNode = isIdentifierName(key) ? t.identifier(key) : t.stringLiteral(key)
119+
const propertySignature = t.tsPropertySignature(keyNode, t.tsTypeAnnotation(type))
122120
propertySignature.optional = attribute.optional
123121

124122
return propertySignature

0 commit comments

Comments
 (0)