diff --git a/README.md b/README.md index 7624a37..c19e31e 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ TypeScript2Python supports many of TypeScripts type constructs, including: ## Transpiler options +### Strict + +Use the `--strict` flag to enable all strict type-checking options to ensure `undefined` and `null` properties are not ignored during transpilation. + ### Nullable optionals In TypeScript objects, optional values can also be set to `undefined`. By default we assume the according Python diff --git a/src/index.ts b/src/index.ts index 2a76405..8477e36 100755 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,15 @@ import path from "path"; import { program } from "@commander-js/extra-typings"; import { typeScriptToPython } from "./typeScriptToPython"; import { Ts2PyConfig } from "./config"; +import { readFileSync } from "fs"; -const compile = (fileNames: string[], config: Ts2PyConfig) => { +const compile = (fileNames: string[], config: Ts2PyConfig & {strict?: boolean}) => { const program = ts.createProgram(fileNames, { noEmit: true, allowJs: true, resolveJsonModule: true, skipLibCheck: true, + strict: config.strict, }); const relevantSourceFiles = program @@ -21,7 +23,6 @@ const compile = (fileNames: string[], config: Ts2PyConfig) => { .map((fn) => path.relative(fn, f.fileName) === "") .reduce((a, b) => a || b), ); - const transpiled = typeScriptToPython(program.getTypeChecker(), relevantSourceFiles, config) console.log(transpiled); } @@ -30,6 +31,7 @@ program .name("typescript2python") .description("A program that converts TypeScript type definitions to Python") .option("--nullable-optionals", "if set, optional entries in dictionaries will be nullable, e.g. `NotRequired[Optional[T]]`") + .option("--strict", "Enable all strict type-checking options.") .arguments("") .action((args, options) => { compile(args, options) diff --git a/src/testing/basic.test.ts b/src/testing/basic.test.ts index eb7ebdc..c0cd21b 100644 --- a/src/testing/basic.test.ts +++ b/src/testing/basic.test.ts @@ -57,11 +57,30 @@ describe("transpiling basic types", () => { "export type T = number | string | Record", "from typing_extensions import Dict, Union\n\nT = Union[str,float,Dict[str,bool]]", ], + [ + "export type T = number | undefined", + // without strict mode the `undefined` gets lost here + "T = float", + ], ])("transpiles %p to %p", async (input, expected) => { const result = await transpileString(input); expect(result).toEqual(expected); }); + it.each([ + [ + "export type T = number | undefined", + "from typing_extensions import Union\n\nT = Union[None,float]", + ], + [ + "export type T = number | null", + "from typing_extensions import Union\n\nT = Union[None,float]", + ], + ])("transpiles %p to %p when strict", async (input, expected) => { + const result = await transpileString(input, {}, { strict: true }); + expect(result).toEqual(expected); + }); + it("only transpiles exported types", async () => { const result = await transpileString(` type NotExported = number; diff --git a/src/testing/dicts.test.ts b/src/testing/dicts.test.ts index 73279b3..7438a1a 100644 --- a/src/testing/dicts.test.ts +++ b/src/testing/dicts.test.ts @@ -64,7 +64,7 @@ class A(TypedDict): expect(result).toContain(`class A(TypedDict):\n foo: NotRequired[str]`); }); - it("transpiles optional values with non-null optionals as NotRequired[T]", async () => { + it.only("transpiles optional values with non-null optionals as NotRequired[T]", async () => { const result = await transpileString(`export type A = { foo?: string }`, { nullableOptionals: true, }); diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 2eb5503..0b136e8 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -8,9 +8,13 @@ import { createProject, ts } from "@ts-morph/bootstrap"; **/ let globalProject: ReturnType | undefined; +/** Each file should get a unique name to avoid issues. */ +let i = 0; + export const transpileString = async ( code: string, config: Ts2PyConfig = {}, + compilerOptions: ts.CompilerOptions = {}, ) => { if (globalProject === undefined) { globalProject = createProject({ @@ -19,11 +23,14 @@ export const transpileString = async ( } const project = await globalProject; - const fileName = `source.ts`; + const fileName = `source_${i++}.ts`; // instead of adding a new source file for each program, we update the existing one. const sourceFile = project.updateSourceFile(fileName, code); - const program = project.createProgram(); + const program = project.createProgram({ + rootNames: [fileName], + options: { ...project.compilerOptions, ...compilerOptions }, + }); const diagnostics = ts.getPreEmitDiagnostics(program); if (diagnostics.length > 0) {