Skip to content

narrow AnyType to StringType in mapped types#2412

Merged
arthurfiorette merged 8 commits intovega:nextfrom
srsudar:sam-any-index-type
Dec 1, 2025
Merged

narrow AnyType to StringType in mapped types#2412
arthurfiorette merged 8 commits intovega:nextfrom
srsudar:sam-any-index-type

Conversation

@srsudar
Copy link
Copy Markdown
Contributor

@srsudar srsudar commented Nov 25, 2025

Overview

The prosemirror library has this bit, which uses any as an index type:

interface SchemaSpec<Nodes extends string = any, Marks extends string = any> {
    nodes: {
        [name in Nodes]: NodeSpec;
    } | OrderedMap<NodeSpec>;

    <snip>
}

This is causing the error shown below when processed by ts-json-schema-generator.

To try and fix this, I'm trying to narrow it to a string.

The error this test generates before the fix:

  ● valid-data-type › index-any-type

    Unexpected key type "any" for this node. (expected "UnionType" or "StringType")

       95 |         }
       96 |
    >  97 |         throw new ExpectationFailedError(
          |               ^
       98 |             `Unexpected key type "${
       99 |                 constraintType ? constraintType.getId() : constraintType
      100 |             }" for this node. (expected "UnionType" or "StringType")`,

      at MappedTypeNodeParser.createType (src/NodeParser/MappedTypeNodeParser.ts:97:15)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at AnnotatedNodeParser.createType (src/NodeParser/AnnotatedNodeParser.ts:34:47)
      at createType (src/NodeParser/InterfaceAndClassNodeParser.ts:146:46)
          at Array.map (<anonymous>)
      at InterfaceAndClassNodeParser.map [as getProperties] (src/NodeParser/InterfaceAndClassNodeParser.ts:142:14)
      at InterfaceAndClassNodeParser.getProperties [as createType] (src/NodeParser/InterfaceAndClassNodeParser.ts:48:33)
      at AnnotatedNodeParser.createType (src/NodeParser/AnnotatedNodeParser.ts:34:47)
      at ExposeNodeParser.createType (src/ExposeNodeParser.ts:23:45)
      at CircularReferenceNodeParser.createType (src/CircularReferenceNodeParser.ts:24:43)
      at ChainNodeParser.createType (src/ChainNodeParser.ts:37:49)
      at TopRefNodeParser.createType (src/TopRefNodeParser.ts:14:47)
      at createType (src/SchemaGenerator.ts:31:39)
          at Array.map (<anonymous>)
      at SchemaGenerator.map [as createSchemaFromNodes] (src/SchemaGenerator.ts:29:33)
      at SchemaGenerator.createSchemaFromNodes [as createSchema] (src/SchemaGenerator.ts:25:21)
      at Object.createSchema (test/utils.ts:63:34)

Version

Published prerelease version: v2.5.0-next.14

Changelog

🎉 This release contains work from new contributors! 🎉

Thanks for all your work!

❤️ Sam Sudar (@srsudar)

❤️ Orta Therox (@orta)

❤️ James Vaughan (@jamesbvaughan)

❤️ Alex (@alexchexes)

❤️ Cal (@CalLavicka)

❤️ Valentyne Stigloher (@pixunil)

🚀 Enhancement

🐛 Bug Fix

🔩 Dependency Updates

Authors: 9

keyListType instanceof NumberType ||
keyListType instanceof SymbolType
keyListType instanceof SymbolType ||
keyListType instanceof AnyType
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do the same for unknown? what are all valid ts.types to index an object?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. With this code:

type Foo = {
  [key: boolean]: unknown;
};

This is the error I get:

An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.

But afaik, everything gets coerced to a string anyways in TS. Eg:

$ node
Welcome to Node.js v20.18.0.
Type ".help" for more information.
> t = {};
{}
> t[1] = 'the number one';
'the number one'
> t[1]
'the number one'
> t['1']
'the number one'
> t
{ '1': 'the number one' }
> t['1'] = 'the string one'
'the string one'
> t
{ '1': 'the string one' }
> t[undefined] = 'this was undefined'
'this was undefined'
> t[undefined]
'this was undefined'
> t['undefined']
'this was undefined'

So I would think that this perhaps could just never throw, and instead always default to string? As-is, this it kind of seems like an additional, more restrictive typecheck.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't keys be symbols (in JS and TS) or string literals (in TS) as well? So it's not always string, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the full list according to the error message:

An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.

But I thought pretty much anything can be a key due to toString behavior. eg:

$ node
Welcome to Node.js v20.18.0.
Type ".help" for more information.
> t = {};
{}
> obj = { hello: 'there'};
{ hello: 'there' }
> t[obj] = 'object value';
'object value'
> t
{ '[object Object]': 'object value' }
> obj.toString()
'[object Object]'

It looks like Symbol does make it into the object, but it doesn't survive JSON.stringify(). Wow, TIL.

I'm not sure what makes the most sense here. I'm pretty new to working with the TS AST like this. Happy to try whatever you think makes sense.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you try Symbol?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think start by making more tests cases with more types (ideally in the same test example rather than creating a ton of examples).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symbol here:

> s = Symbol('symb');
Symbol(symb)
> s
Symbol(symb)
> t[s] = 'symbol value';
'symbol value'
> t
{ '[object Object]': 'object value', [Symbol(symb)]: 'symbol value' }
> JSON.stringify(t)
'{"[object Object]":"object value"}'
>

By "ideally in the same test example rather than creating a ton of examples" do you mean just in a single main.ts and schema.json file?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that you guys had already covered most of these, and in a pattern that I was ignoring.

number is handled here:

https://github.com/vega/ts-json-schema-generator/blob/next/test/valid-data/type-mapped-number/main.ts

symbol is handled here:

https://github.com/vega/ts-json-schema-generator/blob/next/test/valid-data/type-mapped-symbol/main.ts

I copied this for any and for a template literal (which I think are the last two that are uncovered, based on this:

An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.

I stuck both of these in their own files, to keep the pattern going. Lmk if you'd prefer them in a single file. I confirmed that Record<any, string> is failing without this change.

I feel like this shouldn't need to handle unknown, since that isn't valid TS I don't think.

@arthurfiorette arthurfiorette self-assigned this Nov 25, 2025
@arthurfiorette
Copy link
Copy Markdown
Collaborator

LGTM

@arthurfiorette arthurfiorette merged commit 147efdd into vega:next Dec 1, 2025
11 of 12 checks passed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 4, 2026

🚀 PR was released in v2.5.0 🚀

@github-actions github-actions Bot added released This issue/pull request has been released. and removed prerelease labels Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released This issue/pull request has been released.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants