From e5003aed66150c50e787a6dce13e9d213423ba2b Mon Sep 17 00:00:00 2001 From: Lars Melchior Date: Thu, 23 Apr 2026 13:35:51 +0200 Subject: [PATCH] parse symbols as NewTypes --- src/newHelperTypeName.ts | 18 +++++++++++++++++- src/parseInlineType.ts | 24 +++++++++++++++++++----- src/testing/basic.test.ts | 22 +++++++++++++++++++++- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/newHelperTypeName.ts b/src/newHelperTypeName.ts index 1eb7dee..4b3a42c 100644 --- a/src/newHelperTypeName.ts +++ b/src/newHelperTypeName.ts @@ -3,7 +3,7 @@ import ts from "typescript"; import { createHash } from "node:crypto"; import { getCanonicalTypeName } from "./canonicalTypeName"; -export const newHelperTypeName = (state: ParserState, type: ts.Type) => { +export const newHashedHelperTypeName = (state: ParserState, type: ts.Type) => { // to keep helper type names predictable and not dependent on the order of definition, // we use the first 10 characters of a sha256 hash of the type. If there is an unexpected // collision, we fallback to using an incrementing counter. @@ -23,3 +23,19 @@ export const newHelperTypeName = (state: ParserState, type: ts.Type) => { } return `Ts2Py_${shortHash}`; }; + +const getIndexedName = (i: number, prefix: string) => `Ts2Py_${prefix}_${i}`; + +export const newIndexedHelperTypeName = ( + state: ParserState, + type: ts.Type, + prefix: string, +) => { + let i = 0; + while (state.helperTypeNames.has(getIndexedName(i, prefix))) { + i += 1; + } + const name = getIndexedName(i, prefix); + state.helperTypeNames.set(name, type); + return name; +}; diff --git a/src/parseInlineType.ts b/src/parseInlineType.ts index 5a35e16..d417c55 100644 --- a/src/parseInlineType.ts +++ b/src/parseInlineType.ts @@ -1,6 +1,9 @@ import ts, { TypeFlags } from "typescript"; import { ParserState } from "./ParserState"; -import { newHelperTypeName } from "./newHelperTypeName"; +import { + newHashedHelperTypeName, + newIndexedHelperTypeName, +} from "./newHelperTypeName"; import { parseTypeDefinition } from "./parseTypeDefinition"; import { getCanonicalTypeName } from "./canonicalTypeName"; @@ -72,9 +75,20 @@ export const tryToParseInlineType = ( // there is no way to represent template literals in Python, // so we fallback to string return "str"; - } else if (type.flags & TypeFlags.ESSymbol) { - state.imports.add("Any"); - return "Any"; + } else if (type.flags & TypeFlags.ESSymbolLike) { + const knownType = state.knownTypes.get(type); + if (state.knownTypes.has(type)) { + return knownType; + } else { + // we must create a new type to represent the symbol + state.imports.add("NewType"); + const name = newIndexedHelperTypeName(state, type, "symbol"); + state.statements.push( + `${name} = NewType(${JSON.stringify(name)}, object)`, + ); + state.knownTypes.set(type, name); + return name; + } } else { // assume interface or object, we need to create a helper type if (!globalScope) { @@ -85,7 +99,7 @@ export const tryToParseInlineType = ( return semanticallyIdenticalType; } else { // we must create a new type - const helperName = newHelperTypeName(state, type); + const helperName = newHashedHelperTypeName(state, type); parseTypeDefinition(state, helperName, type); state.knownTypes.set(canonicalName, helperName); return helperName; diff --git a/src/testing/basic.test.ts b/src/testing/basic.test.ts index be80b75..afdf0c0 100644 --- a/src/testing/basic.test.ts +++ b/src/testing/basic.test.ts @@ -62,7 +62,10 @@ describe("transpiling basic types", () => { "from typing_extensions import Union\n\nT = Union[None,float]", ], ["export type T = `a.b.${string}`", "T = str"], - ["export type T = symbol", "from typing_extensions import Any\n\nT = Any"], + [ + "export type T = symbol", + 'from typing_extensions import NewType\n\nTs2Py_symbol_0 = NewType("Ts2Py_symbol_0", object)\n\nT = Ts2Py_symbol_0', + ], ])("transpiles %p to %p", async (input, expected) => { const result = await transpileString(input); expect(result).toEqual(expected); @@ -93,4 +96,21 @@ describe("transpiling basic types", () => { expect(result).not.toContain("exported"); expect(result).toContain("Exported = float"); }); + + it("re-uses existing symbol definitions", async () => { + const result = await transpileString(` + const a = Symbol("a") + const b = Symbol("b") + export type T1 = symbol; + export type T2 = symbol; + export type T3 = typeof a; + export type T4 = symbol; + export type T5 = typeof b; + `); + expect(result).toContain(`T1 = Ts2Py_symbol_0`); + expect(result).toContain(`T2 = Ts2Py_symbol_0`); + expect(result).toContain(`T3 = Ts2Py_symbol_1`); + expect(result).toContain(`T4 = Ts2Py_symbol_0`); + expect(result).toContain(`T5 = Ts2Py_symbol_2`); + }); });