CodeFixesHub
    programming tutorial

    Typing Functions That Use new Function() (A Cautionary Tale)

    Learn risks, typing patterns, and safe alternatives to new Function() with examples, typing strategies, and next steps—secure your code today.

    article details

    Quick Overview

    TypeScript
    Category
    Sep 3
    Published
    18
    Min Read
    2K
    Words
    article summary

    Learn risks, typing patterns, and safe alternatives to new Function() with examples, typing strategies, and next steps—secure your code today.

    Typing Functions That Use new Function() (A Cautionary Tale)

    Introduction

    Using JavaScript's new Function() is a powerful but dangerous tool. It allows dynamic function creation from strings at runtime, which can be tempting for DSLs, plugin systems, or dynamic serialization. For intermediate developers working with TypeScript or strict codebases, mixing new Function() with static typing raises immediate challenges: how do you maintain type safety, enable reliable editor tooling, and avoid runtime surprises when function shapes are generated at runtime from strings?

    In this article you'll learn why new Function() is risky, how to reason about types for dynamic functions, techniques to provide safe typings and fallbacks, and practical alternatives that retain developer experience and runtime security. We'll walk through a step-by-step example of a tiny expression-evaluator that initially uses new Function(), then show how to: add narrow TypeScript types, runtime validation, safer invocation wrappers, and migration strategies away from string-evaluated code.

    You'll also get hands-on patterns for integrating middleware, using JSDoc in mixed JS/TS code, using type predicates for runtime narrowing, and explicit contracts between runtime-generated code and its callers. Throughout, we'll highlight security implications and link to deeper typing topics like optional object parameters, variadic args, type predicates, and configuration typing.

    By the end you should be able to identify when new Function() is appropriate, how to type the surrounding API safely, and how to migrate dangerous runtime string-evaluation to more maintainable and testable approaches.

    Background & Context

    new Function() constructs a function from textual source at runtime: new Function(arg1, arg2, 'return arg1 + arg2') produces a callable Function. That dynamism defeats many of TypeScript's benefits: compile-time checks, inference, and the guarantee that a function adheres to its signature. Moreover, new Function() is effectively equivalent to eval in many environments and can be a vector for injection attacks when used with untrusted input.

    Understanding how to type functions that originate from strings involves a mixture of static typing for the surrounding API, runtime validation of generated code, and careful design of boundaries between trusted and untrusted contexts. For functions that rely on external context (the runtime this), see our guide on typing functions with context (the this type) for patterns to safely describe execution context in TypeScript.

    Key Takeaways

    • new Function() creates functions from strings at runtime and bypasses TypeScript's guarantees
    • Always treat string-generated code as untrusted input unless strictly controlled
    • Add static interfaces around the dynamic boundary so callers get typed contracts
    • Use runtime validation and type predicates to assert shapes after invocation
    • Prefer safer alternatives (AST interpreters, DSLs, precompiled templates) when possible
    • Use JSDoc or declaration files when migrating mixed JS/TS code

    Prerequisites & Setup

    You'll need node (v14+ recommended) and TypeScript (v4.5+ recommended). A code editor with TypeScript support (VS Code) will help with inference and quick iteration. Clone a simple repo or create a new npm project and install typescript as a dev dependency:

    bash
    npm init -y
    npm install --save-dev typescript @types/node
    npx tsc --init

    If you have mixed JavaScript files that must remain JS, consider JSDoc annotations for type checking; see our guide on using JSDoc for type checking JavaScript files for practical patterns.

    Main Tutorial Sections

    1) A Minimal Example: Dynamic Adder

    Let's start with a simple runtime example. Imagine building an expression system that accepts an expression string and returns a function:

    ts
    // evaluator.ts
    export function makeAdder(expr: string) {
      // dangerously create a function that expects two numbers
      return new Function('a', 'b', `return ${expr}`) as (...args: any[]) => any;
    }
    
    const fn = makeAdder('a + b');
    console.log(fn(1, 2)); // 3

    This works at runtime, but TypeScript sees a cast to any-function. That loses parameter types, return types, and editor help. Next we'll add static typing for the public API while keeping the dynamic implementation.

    2) Adding a Typed API Surface

    We can give callers a typed interface without trusting the internals. Define a generic signature describing the expected inputs and output:

    ts
    export type BinaryNumberOp = (a: number, b: number) => number;
    
    export function makeTypedAdder(expr: string): BinaryNumberOp {
      return new Function('a', 'b', `return ${expr}`) as BinaryNumberOp;
    }

    This gives consumers type safety for calls to makeTypedAdder, but it still assumes the generated code will behave. Use this only when you fully control the input expression. If user input is involved, you must validate.

    3) Runtime Validation: Guarding the Output

    A robust pattern is to validate results at runtime. Use type predicates to narrow values and catch unexpected types. See using type predicates for custom type guards for more examples.

    ts
    function isNumber(x: unknown): x is number {
      return typeof x === 'number' && Number.isFinite(x);
    }
    
    export function safeMakeAdder(expr: string): BinaryNumberOp {
      const fn = new Function('a', 'b', `return ${expr}`) as (...args: unknown[]) => unknown;
      return (a: number, b: number) => {
        const out = fn(a, b);
        if (!isNumber(out)) {
          throw new Error('Expression did not evaluate to a number');
        }
        return out;
      };
    }

    This wrapper preserves a typed contract while enforcing it at runtime.

    4) Dealing with Variable Arguments

    If generated functions accept variable arguments, typing becomes trickier. You can use rest tuples and overloads to model varargs. For complex variadic behavior, consult patterns in typing functions with variable number of arguments (rest parameters revisited).

    Example: create a function that compiles an expression with rest args:

    ts
    export type VarFn = (...args: number[]) => number;
    
    export function makeVarFn(expr: string): VarFn {
      // build arg list 'a0','a1',... based on runtime arity
      return new Function('...args', `return (${expr})`) as VarFn;
    }

    Be careful: the call signature is generic but the body might assume specific positions.

    5) Safe DSL: A Small Interpreter Instead of new Function()

    When user input drives evaluation, prefer an interpreter or parsed AST over string evaluation. Implement a small expression language that parses tokens and evaluates safely. This preserves type safety and is auditable.

    Simple example:

    ts
    type Expr = { kind: 'add', left: Expr|number, right: Expr|number } | number;
    
    function evalExpr(e: Expr): number {
      if (typeof e === 'number') return e;
      return evalExpr(e.left) + evalExpr(e.right);
    }

    Interpreters let you validate AST shapes with TypeScript types and provide safer runtime guarantees.

    6) Typing Function Factories That Produce Promises

    If dynamic functions return Promises or different result types, capture that in the API. See our guide on typing Promises that resolve with different types for helpful patterns.

    ts
    export type AsyncOp = (...args: any[]) => Promise<number>;
    
    export function makeAsyncOperation(expr: string): AsyncOp {
      const fn = new Function('...args', `return Promise.resolve(${expr})`) as AsyncOp;
      return fn;
    }

    Add runtime checks to verify the resolved type using predicates.

    7) Using as const to Lock Down Literal Inputs

    When you pass small sets of literal options to dynamic builders, lock them with 'as const' so TypeScript can infer literal types. This helps when constructing the function signature or permitted operations. See using as const for literal type inference for more patterns.

    Example:

    ts
    const ops = ['+', '-', '*'] as const;
    type Op = typeof ops[number];
    
    function buildOp(op: Op) { return new Function('a','b', `return a ${op} b`) as (a:number,b:number)=>number; }

    Literal typing narrows accepted operators at compile time.

    8) Using Runtime Contracts and Exact Object Shapes

    If dynamic functions interact with configuration objects, define exact shapes for those objects so callers and implementers agree. Check out techniques to prevent excess properties in typing objects with exact properties in TypeScript to make APIs less error-prone.

    Example contract:

    ts
    type EvalConfig = { maxOps: number } & Record<string, unknown>;
    
    function makeEvaluator(config: EvalConfig) {
      // Use config to sanitize or limit expression features
    }

    Contracts help you check user-provided options before passing them to the generator.

    9) Migration Strategy: From new Function() to Safer Patterns

    When maintaining legacy code that uses new Function(), adopt a staged plan: (1) add typed wrappers and runtime guards, (2) add logging and tests around generated code, (3) introduce a parser/interpreter for user-controlled expressions, and (4) deprecate string evaluation.

    During migration, use JSDoc or declaration files for JS modules to keep editor checks while you refactor. See our article on using JSDoc for type checking JavaScript files for concrete steps.

    Advanced Techniques

    Beyond basic guards, there are expert strategies to manage dynamic code safely. Use sandboxing (isolated processes or realms) to run untrusted code, limit available globals, and strictly control the runtime surface. For Node.js, run dynamic code in worker threads or child processes and communicate over IPC to reduce blast radius.

    Another advanced pattern is to compile expressions to a safe subset of JavaScript (an AST-to-bytecode or AST-to-safe-eval that only supports allowed nodes). Use existing parsers (acorn, esprima) to transform and validate AST nodes before turning them into callable logic. Combine this with precise TypeScript declaration files for the generated functions so consumers retain a typed experience while runtime safety is enforced.

    Finally, consider code generation at build time. If expressions are known ahead of deployment, precompile them into modules to retain static typing and avoid runtime string evaluation entirely.

    Best Practices & Common Pitfalls

    Do:

    • Treat input used in new Function() as untrusted unless explicitly controlled.
    • Provide a typed API surface so consumers get compile-time guarantees.
    • Add runtime validation using type predicates and explicit checks.
    • Prefer interpreters or AST-driven execution for user-provided code.
    • Lock literals with 'as const' when feasible to narrow acceptable values.

    Don't:

    • Cast everything to any and assume runtime will behave.
    • Expose new Function-based APIs directly to users without validation.
    • Ignore performance costs of repeated string parsing or dynamic compilation.

    Common pitfalls:

    • Silent runtime errors due to mismatched expectations between the typed API and actual dynamic function behavior. Always wrap calls and validate outputs.
    • Performance: constructing functions per request can be expensive. Cache compiled functions keyed by expression if expressions repeat.
    • Security: injection vulnerabilities if you compose expressions from user input. Use sanitizers or parsers to remove dangerous constructs.

    For a deep look at the security tradeoffs of using any and type assertions when working with dynamic code, read security implications of using any and type assertions in TypeScript.

    Real-World Applications

    There are legitimate use cases where runtime-generated functions are useful: template engines, plugin sandboxes, dynamic query builders for internal tooling, and quickly-iterating developer utilities. In each case, follow the patterns above: narrow the API surface, validate inputs, sandbox execution, and prefer safer alternatives where user input is involved.

    For plugin systems where chaining APIs are generated at runtime, consider adopting established method-chaining typing patterns to keep DX intact. Our guide on typing libraries that use method chaining in TypeScript can help design fluent APIs that remain type-safe.

    Conclusion & Next Steps

    new Function() can be convenient but it's a challenging fit for statically typed systems. Provide typed APIs, guard outputs, and prefer interpreters or precompiled functions for untrusted input. Next steps: audit your codebase for dynamic eval patterns, add runtime guards, and read about type predicates and JSDoc patterns to improve mixed JS/TS projects.

    Explore related articles on precise typing patterns such as typing functions with multiple return types (union types revisited) and ensure your configuration objects are correctly typed with typing configuration objects in TypeScript.

    Enhanced FAQ

    Q: Why is new Function() considered dangerous compared to normal function expressions? A: new Function() evaluates a string as code, bypassing the static checks TypeScript provides and re-introducing the risks of injection, unbounded execution, and runtime errors. Unlike a function literal, new Function() parses and compiles at runtime which can be manipulated if inputs are uncontrolled.

    Q: Can I safely use new Function() if I validate inputs? A: You can mitigate many risks by validating inputs, sanitizing expressions, and restricting capabilities. However, validation is tricky: naive string sanitization can be bypassed. Prefer parsing into an AST and whitelisting node types, or use an interpreter for user-provided input.

    Q: How do you type a function constructed with new Function() in TypeScript? A: Expose a typed wrapper that returns the desired signature. Internally you can cast the generated function to that type, but you must validate at runtime to ensure the function adheres to the contract. Example: annotate return type as (a:number,b:number)=>number and wrap calls with runtime checks.

    Q: What tools help validate dynamically generated code? A: Use parsers like acorn/esprima to parse and inspect ASTs, run static AST validation to reject dangerous constructs, and use worker threads or sandboxed processes to execute untrusted code. Combine with type predicates in TypeScript to assert runtime shapes after execution.

    Q: How should I handle functions created from strings that return different possible types? A: Model the return as a union type and use runtime narrowing with type predicates or discriminant properties. If asynchronous, use typed Promises and validate resolved values. See our article on typing Promises that resolve with different types for guidance.

    Q: Are there performance concerns with new Function()? A: Yes—creating and compiling functions at runtime is heavier than executing precompiled code. If expressions repeat, cache compiled functions keyed by the expression. If build-time compilation is possible, prefer that to reduce runtime overhead.

    Q: How do I migrate a codebase that heavily uses new Function()? A: Start by wrapping existing uses with typed APIs and runtime guards, add tests and logging for generated code, then gradually replace user-facing dynamic evaluation with interpreters or compile-time generation. Use JSDoc to maintain type hints for JS modules during migration; read using JSDoc for type checking JavaScript files for migration tips.

    Q: What TypeScript features can help when designing typed wrappers? A: Use generics to describe param and return shapes, 'as const' to preserve literal types when building small DSLs, and type predicates for runtime narrowing. For method chaining or fluent APIs produced dynamically, look at patterns from typing libraries that use method chaining in TypeScript.

    Q: How to test dynamic functions reliably? A: Unit-test wrapper logic, test with a variety of expression inputs including edge cases, and simulate malicious inputs to ensure sanitizers and parsers reject them. Use contract tests that assert typed return shapes and behavior under failure scenarios.

    Q: Should I ever use new Function() in production? A: Only when alternatives are impractical and the input is strictly controlled by the application (not end users). When used, combine strict validation, sandboxing, caching, and clear contracts with type-safe wrappers. For most user-driven features, prefer AST interpretation or precompilation.

    Further reading: to avoid common typing pitfalls when you must accept flexible inputs, check our guides on typing arrays of mixed types and typing functions with optional object parameters for patterns you can adopt in related APIs.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:20:02 PM
    Next sync: 60s
    Loading CodeFixesHub...