Gradual typing
Gradual typing is a type system in programming languages that bridges static and dynamic typing by allowing optional type annotations, where unannotated code behaves dynamically while annotated portions receive static checks, enabling a programmer-controlled migration between typing disciplines.[1] The concept originated in 2006 with the work of Jeremy G. Siek and Walid Taha, who formalized gradual typing in their paper "Gradual Typing for Functional Languages," presenting a simply-typed lambda calculus extension called λ?→ that incorporates a dynamic type "?" to represent unknown types and supports structural subtyping.[1] This foundation proved type safety for the system and introduced principles of behavior preservation upon adding annotations, later formalized as the "gradual guarantee" ensuring that adding type annotations to a dynamically correct program preserves its observable behavior.[2] Gradual typing addresses the trade-offs between dynamic languages, which prioritize rapid development and flexibility but risk runtime errors, and static languages, which enhance reliability and performance through compile-time verification but impose upfront annotation burdens.[1] Key benefits include early error detection in typed regions, optimized execution via unboxed representations for static parts, and incremental adoption that incurs dynamic checks only at type boundaries, embodying a "pay-as-you-go" cost model.[1] Research has since refined criteria like the gradual guarantee to evaluate soundness, distinguishing between blame-tracking semantics that isolate type errors and performance-oriented implementations.[2] Notable implementations include Typed Racket, a dialect of Racket using higher-order contracts for sound gradual typing and supporting large-scale migration from dynamic to static code; TypeScript, a superset of JavaScript that employs theany type for gradual adoption and structural typing; and Dart, a language with static type checking and a dynamic type that facilitates optional typing in web and mobile development.[3][4][5] These systems demonstrate gradual typing's practicality in production environments, influencing languages like Hack for PHP[6] and ActionScript,[7] while ongoing work explores optimizations, semantics variations (e.g., deep vs. shallow types), and empirical performance evaluations.[8][9]
Core Concepts
Definition and Principles
Gradual typing is a hybrid type system in programming languages that integrates static type checking at compile time with dynamic type checking at runtime, permitting both typed and untyped code to coexist within the same program.[1] This approach introduces a special type, often denoted as? or dynamic, which represents unknown or untyped values and serves as a bridge between statically typed and dynamically typed regions.[1] Unlike purely static systems, gradual typing does not require all code to be annotated upfront, allowing programmers to control the degree of type enforcement on a per-module or per-function basis.[2]
The core principles of gradual typing emphasize incremental integration, enabling developers to gradually add type annotations to existing dynamically typed code without necessitating a complete rewrite.[2] This supports mixed-typed programs where static checks verify type correctness in annotated sections, while runtime checks occur only at the boundaries between typed and untyped code to ensure safe interactions.[1] Motivated by the limitations of pure systems—static typing's rigid constraints that make refactoring error-prone and dynamic typing's tendency to detect errors late at runtime—gradual typing facilitates a smooth evolution from dynamic to static typing, balancing early error detection with prototyping flexibility.[10]
In terms of basic semantics, untyped code interacts with typed code through mechanisms such as runtime casts or proxies that enforce type contracts at interaction points, preserving type safety without altering the behavior of purely static or dynamic subsets.[1] These boundary checks, often implemented as dynamic casts from the unknown type ? to specific types, catch type mismatches at the earliest possible runtime moment while allowing untyped regions to execute freely.[2] This "pay-as-you-go" approach ensures that the performance and safety benefits of static typing apply where annotations are present, with minimal overhead elsewhere.[1]
Comparison with Other Typing Systems
Gradual typing occupies a hybrid position between pure static typing systems, which enforce comprehensive compile-time type checks across all code, and pure dynamic typing systems, which defer all type verification to runtime. In pure static typing, as exemplified by languages like Java, every variable and expression must be fully annotated or inferred at compile time, ensuring no runtime type errors but imposing significant upfront annotation effort and reducing flexibility for rapid prototyping.[11] Gradual typing contrasts by allowing optional type annotations, where unannotated code (often denoted by a dynamic type like?) is treated dynamically, enabling developers to add static checks incrementally without requiring exhaustive annotations from the outset.[2]
Pure dynamic typing, seen in languages such as Python, relies entirely on runtime type checks, offering high flexibility and ease of development but prone to late error detection and potential performance overhead from ubiquitous tagging and verification.[11] Gradual typing builds on this foundation by incorporating optional static type annotations that provide early error catching and optimization opportunities for annotated portions, without eliminating the dynamic behavior for unannotated code, thus preserving prototyping speed while enhancing safety progressively.[11]
A related but distinct approach is optional typing, where type annotations serve primarily as hints for tools like IDEs and are ignored at runtime, as in early versions of TypeScript.[2] In contrast, gradual typing enforces runtime consistency through mechanisms like casts or blame tracking, ensuring that interactions between static and dynamic code are monitored to prevent uncaught type errors, thereby providing stronger safety guarantees beyond mere documentation.[2]
Within gradual typing itself, implementations vary between sound and unsound variants. Sound gradual typing, as in Typed Racket, provably ensures no runtime type errors in well-typed programs by inserting precise dynamic checks and blame assignment, though this can introduce measurable overhead at type boundaries.[3] Unsound gradual typing, exemplified by TypeScript, prioritizes practicality by omitting some runtime enforcement, allowing potential type errors to surface at runtime for better performance and developer experience, but without formal safety proofs.[12]
Gradual typing also differs from more advanced hybrid systems like dependent types, which encode runtime properties directly into types for fine-grained verification, or flow-sensitive typing, which refines types based on control flow. Unlike these, gradual typing focuses on a seamless blend of static and dynamic disciplines via optional annotations and consistency relations, without requiring value-dependent type expressions or path-specific refinements.[2]
Theoretical Foundations
Consistency Relation
In gradual typing, the consistency relation, denoted \sim, serves as a core mechanism to enable safe interoperability between statically typed and dynamically typed code by relating types in a way that permits values to flow across type boundaries without immediate failure. This relation is defined such that two types are consistent if their known components match structurally, while components marked as unknown (typically denoted by the dynamic type \?) are ignored in the comparison, allowing any static type to be consistent with \?. For instance, \Int \sim \? and \String \sim \?, enabling typed values to be treated dynamically and vice versa. The relation is reflexive, meaning every type is consistent with itself (A \sim A), and symmetric, so if A \sim B then B \sim A, but it is deliberately not transitive to avoid unsoundness— for example, while \Int \sim \? and \? \sim \String, it does not follow that \Int \sim \String, as this would permit unsafe implicit conversions without checks.[1][2] The consistency relation plays a pivotal role in static type checking within gradual type systems, such as the gradually typed lambda calculus (GTLC), by replacing strict equality checks at boundaries between typed and untyped regions. During type inference or checking, it allows a program to be well-typed if the argument type of a function application is consistent with the expected parameter type (e.g., in the application rule, if the type of the argument T_2 \sim T_{11}, where T_{11} is the domain of the function's type). This facilitates gradual refinement, where developers can incrementally add type annotations to untyped code without breaking existing functionality, as inconsistencies are deferred to runtime rather than rejected statically. Computationally, consistency is determined through structural matching: for base types, it checks equality unless involving \?, which matches universally; for function types, it recursively ensures the argument types are consistent and the result types match exactly (or involve \? appropriately); and for more complex types like records or unions, it aligns corresponding fields while treating unknown parts as wildcards.[1][2] At runtime, the consistency relation is enforced dynamically through mechanisms like cast insertion or proxy objects, which monitor and validate interactions across type boundaries. For example, a cast from a static type to \? (an upcast) is typically safe and identity-like, while a cast from \? to a static type (a downcast) may fail if the dynamic value does not conform, triggering error handling. This enforcement ties into the blame calculus, an extension of the GTLC that introduces blame assignment to attribute type errors precisely to the source of inconsistency. In the blame calculus, casts are labeled with sources (e.g., blame labels p), and failures are assigned "positive blame" to the dynamic side introducing the error (e.g., untyped code providing an incompatible value) or "negative blame" to the context, ensuring that well-typed (statically precise) code is never blamed—a property formalized as the blame theorem or gradual guarantee. Blame tracking thus promotes debugging by localizing faults to less-precisely typed regions, reinforcing the safety of gradual typing.[1][13][14]Integration with Subtyping
In gradual typing systems, traditional subtyping relations, such as width and variance rules for records or objects, apply exclusively among static types and remain unaffected by the presence of dynamic types.[1] This preserves the standard subtyping rules from statically typed languages, where, for instance, function subtyping enforces contravariance in arguments and covariance in return types (e.g., if \sigma_1 <: \tau_1 and \tau_2 <: \sigma_2, then \tau_1 \to \tau_2 <: \sigma_1 \to \sigma_2).[15] The dynamic type, often denoted as ? ordyn, functions as a top type in static contexts but does not alter these rules, ensuring that subtyping judgments are decidable and confined to fully annotated components.[16]
A core principle in this integration is the separation between the consistency relation (~) and subtyping (<:), where consistency governs interactions involving dynamic types—such as allowing a dynamic value to be used where a static type is expected, subject to runtime validation—while subtyping operates solely on static types.[15] This separation, formalized through consistent subtyping (≲), combines the two relations orthogonally: A \lesssim B holds if there exist static approximations A' and B' such that A <: A' \sim B' <: B.[15] By design, this avoids monotonicity issues, where refining a type (e.g., replacing ? with a more precise static type) could unexpectedly break subtyping chains or introduce inconsistencies in mixed static-dynamic code.[15] As a brief reference, the consistency relation from prior theoretical foundations ensures safe dynamic-static bridging without overlapping with static subtyping mechanics.[1]
Gradual typing extends support for polymorphism, particularly generics, by treating the dynamic type as a wildcard that can instantiate type parameters, thereby maintaining principal types in inference.[17] For example, in a generic class like Cell<T>, substituting dyn for T (as in Cell<dyn>) allows compatibility with any concrete instantiation via a bidirectional relation, where Cell<Rectangle> . Cell<dyn> and vice versa, enabling flexible use while preserving type safety through runtime checks.[17] Bounded variants, such as dyn<Shape>, further restrict wildcards to subtypes of a given upper bound, ensuring that polymorphic types remain principal and avoid over-approximation in gradual inference.[17] This approach gradualizes subtyping polymorphism semantically, using materialization to map dynamic types to sets of static approximations, thus integrating seamlessly with Hindley-Milner-style polymorphism.[16]
In object-oriented contexts, subsumption for dynamic types permits untyped objects to be treated as subtypes of static interfaces, with runtime checks enforcing compatibility.[18] Optimistic subtyping allows the dynamic type to subsume any static type (dynamic ◃ C for any class C), while pessimistic subtyping treats it as a supertype (dynamic ◂ ⊤), enabling dynamic objects to fulfill static contracts via nominal runtime tags and casts at method boundaries.[18] These checks, such as verifying interface conformance during dispatch, maintain the gradual guarantee without wrappers, supporting features like inheritance and overloading in nominally typed gradual systems.[18]
Historical Development
Origins and Early Research
The concept of gradual typing emerged as a response to the limitations of purely dynamic and static typing systems, drawing inspiration from earlier efforts to blend type checking approaches in functional and scripting languages. In the early 1990s, researchers explored "soft typing" as a way to approximate static type information in dynamically typed languages like Scheme without requiring full type annotations or rejecting untypable code. Robert Cartwright and Andrew Wright introduced a practical soft type system for Scheme in 1994, which inferred types conservatively and inserted runtime checks to handle ambiguities, thereby providing partial static safety while maintaining the flexibility of dynamic typing. This work highlighted the potential for hybrid verification in untyped environments, influencing later developments in gradual systems by demonstrating how type inference could mitigate runtime errors without rigid enforcement. The term "gradual typing" was coined in 2006 by Jeremy Siek and Walid Taha in their foundational paper on integrating static and dynamic typing within a single language, initially applied to functional languages similar to ML.[1] Siek and Taha proposed a type system where programmers could optionally add type annotations, with the type checker treating unannotated parts as dynamically typed via a special "?" type that deferred checks to runtime. This design allowed for incremental adoption of types, motivated by the need to retrofit static guarantees onto existing dynamic codebases, particularly in scripting languages where rapid prototyping was prioritized over upfront type declarations. Their approach addressed the growing recognition in the mid-2000s that dynamic languages like Scheme were increasingly used for large-scale applications, yet suffered from scalability issues due to unchecked errors.[1] Building on this foundation, early formalizations of gradual typing emphasized safe interoperability between typed and untyped code through the notion of blame assignment. In 2006, Sam Tobin-Hochstadt and Matthias Felleisen introduced interlanguage migration techniques for Scheme, enabling the gradual introduction of types into untyped modules while ensuring type safety via contracts that track error origins.[19] This work laid the groundwork for blame, a mechanism to attribute runtime type errors to either the static or dynamic side of the boundary. By 2008, Tobin-Hochstadt and Felleisen formalized the blame calculus in the context of Typed Racket, the first practical implementation of gradual typing, which extended PLT Scheme with optional type annotations and proved that well-typed gradual programs could not be blamed for errors originating in untyped code.[20] Typed Racket's development from 2006 to 2008 demonstrated the feasibility of retrofitting types to dynamic languages, driven by the PLT Scheme community's interest in enhancing safety for evolving software systems without disrupting existing untyped libraries.[20]Key Milestones and Adoption
In the mid-2010s, research on gradual typing advanced significantly with the formalization of key guarantees for type safety and behavior preservation. A seminal contribution was the 2015 paper "Refined Criteria for Gradual Typing" by Jeremy G. Siek, Michael M. Vitousek, Matteo Cimini, and John Tang Boyland, which introduced the "gradual guarantee"—a property ensuring that adding or refining type annotations in a gradually typed program does not alter its observable behavior unless a type error is detected, providing a rigorous foundation for safe interoperability between typed and untyped code.[21] This work built on earlier criteria and became a benchmark for evaluating gradual type systems. Concurrently, efforts expanded gradual typing to object-oriented paradigms; in 2016, Ronald Garcia, Alison M. Clark, and Éric Tanter's POPL paper "Abstracting Gradual Typing" proposed a semantic framework using abstract interpretation to handle gradual types in OO languages, enabling monotonic refinement of types while preserving program semantics and supporting features like subtyping.[22] Industry adoption of gradual typing gained momentum during this period, integrating it into popular languages to bridge dynamic scripting with static safety. Microsoft launched TypeScript in 2012 as a superset of JavaScript with optional static types, but its gradual typing features matured by 2015, allowing seamless mixing of typed and untyped code through structural typing and type erasure to JavaScript, as demonstrated in the POPL 2015 paper "Safe & Efficient Gradual Typing for TypeScript" by Aseem Rastogi et al., which validated its usability on large codebases.[23] Similarly, Facebook introduced Hack in 2014 as a dialect of PHP with gradual static typing via the Hack type checker (HackC), enabling incremental adoption on PHP's dynamic foundation while catching errors early in production-scale applications.[6] For C#, the dynamic keyword was added in version 4.0 (2010), providing runtime type resolution akin to dynamic languages; however, it lacks the blame-tracking safety of true gradual typing systems, though later versions like .NET 5 (2020) improved interop through features such as nullable reference types. Post-2020 developments highlighted broader integration into diverse ecosystems, particularly in scripting and game development. In Python, gradual typing support evolved through PEP 484 (accepted 2014, implemented in Python 3.5) for type hints and PEP 563, with postponed evaluation of annotations implemented as standard in Python 3.14 (released October 2025), enabling optional static checking via tools like mypy, which enforces types gradually without runtime overhead and has been adopted in major projects for refactoring large dynamic codebases. The Godot game engine enhanced gradual static typing in GDScript with Godot 4.0 (released March 2023), adding full type inference, variant handling, and performance boosts for typed code, allowing developers to mix dynamic scripting with static checks for better IDE support and error detection in game logic.[24] Emerging experiments in Rust, a strictly static language, include crates like structural-typing (published November 2025), which introduces optional fields and gradual structural typing inspired by TypeScript, and glowstick for shape-checked tensors, signaling tentative explorations of gradual features via libraries despite Rust's core design.[25] Research in the late 2010s and 2020s shifted toward scalability and robustness, emphasizing verification techniques for large-scale gradual systems. From 2018 onward, papers like "Migrating Gradual Types" (POPL 2018) by John Peter Campora III, Sheng Chen, Martin Erwig, and Eric Walkingshaw addressed performance in migrating untyped code to typed, showing linear scaling in type checking for programs up to millions of lines, influencing tools for industrial migration. Ongoing work from 2018–2025 has focused on verification, such as gradual program verification frameworks that combine static proofs with dynamic checks for partial specifications. Blame tolerance and error recovery emerged as key concerns, with the 2022 ICFP paper "A Reasonably Gradual Type Theory" by Daniel Patterson and David Thrane Christiansen introducing GRIP, a dependently typed system with internal precision and exception handling to tolerate imprecise types gracefully, reducing abrupt failures in mixed-type interactions.[26]Practical Implementations
Language Design Approaches
Gradual typing can be incorporated into existing dynamic languages through retrofitting, where optional type annotations are added to support both static checking and runtime enforcement of types. This approach typically involves source-to-source translation or instrumentation to insert dynamic checks at boundaries between typed and untyped code, often using mechanisms like transient casts or proxies to monitor and enforce type consistency without altering the underlying language runtime. For instance, in Reticulated Python, a source-to-source translator converts annotated Python code to standard Python 3, inserting runtime casts to handle type transitions, which allows gradual migration but introduces trade-offs such as potential performance overhead during incremental adoption and challenges in verifying third-party untyped libraries.[27][28] Migration paths must balance preserving dynamic flexibility for prototyping with the safety of static checks, often requiring phased annotation of critical modules to minimize disruption, though untyped code can propagate errors into typed sections unless bounded by explicit checks.[27] For new languages designed with gradual typing from the outset, the type system integrates a dynamic type—such as the ? operator in early functional designs or Raku's Any type—natively to enable seamless mixing of static and dynamic code without retrofitting overhead. This ground-up support allows the dynamic type to serve as a default for unannotated elements, facilitating optional annotations while embedding consistency relations to relate static types to the unknown dynamic type. In Raku, Any acts as the root for unspecified types, promoting practical flexibility through coercions and mixins while maintaining soundness by throwing exceptions on type check failures to provide clear error feedback.[1][29][30] Decisions on soundness vary: provably sound systems enforce full type safety via runtime casts that halt on errors, whereas practical designs like Raku's prioritize developer-friendly error reporting through exceptions to avoid silent failures.[1][29] Tooling integration plays a key role in gradual typing designs, with type checkers either operating as separate tools or embedded via compiler flags to support optional static analysis. Separate checkers, such as Flow for JavaScript, run independently to infer and verify types across mixed codebases, providing fast feedback without modifying the language runtime and allowing untyped code to bypass checks entirely.[31] In contrast, built-in approaches like Hack's compiler flags enable mode-switching between dynamic PHP compatibility and strict static typing, integrating checks directly into the compilation process for immediate enforcement on annotated files.[32] This distinction affects usability: separate tools offer loose coupling for legacy code but require explicit invocation, while built-in flags streamline workflows in controlled environments like server-side applications.[33] Design trade-offs in gradual typing center on balancing expressiveness with simplicity, particularly in supporting features like union types to model the dynamic unknown while managing interactions across modules and libraries. Union types enhance expressiveness by representing the ? type as a union over all static types, allowing flexible subtyping but complicating inference and check insertion due to exponential growth in type combinations.[1] Simplicity is preserved by limiting unions to gradual contexts or using semantic subtyping to avoid overly permissive behaviors, though this can reduce the ease of migrating untyped libraries. Handling modules involves boundary checks at imports—such as contracts or proxies—to isolate typed code from untyped dependencies, trading off interoperability for soundness; for example, open-world designs assume untyped libraries may violate contracts, inserting pervasive runtime monitors to protect typed modules without requiring full retyping.[28] These choices prioritize developer productivity in large codebases, often favoring practical unsoundness over exhaustive guarantees to accommodate evolving libraries.[33]Performance Considerations
Gradual typing introduces runtime overhead primarily through dynamic checks inserted at the boundaries between typed and untyped code, often in the form of casts or coercions to enforce type safety. These checks verify that values crossing the boundary conform to expected types, preventing type errors from propagating incorrectly, but they can lead to significant slowdowns in mixed programs, with early implementations like Typed Racket reporting up to 100× overhead in partially typed configurations.[34][35] In sound gradual typing systems, blame computation further contributes to this cost by tracking the origin of type errors—assigning responsibility to either typed or untyped code—through annotations on casts, which requires additional metadata storage and propagation during execution.[36][37] Optimization strategies mitigate these overheads by targeting redundant or predictable checks. For instance, Typed Racket leverages Racket's just-in-time (JIT) compiler to optimize contracts generated from types at module boundaries, specializing and inlining checks where possible to reduce invocation costs in hot paths.[38] Similarly, techniques like monotonic references enable elision of runtime checks in monomorphic, statically typed code by ensuring that once a value is verified, subsequent accesses avoid proxy wrappers, imposing no dynamic typing overhead in fully typed regions.[39] Space-efficient coercion composition further reduces overhead by merging adjacent casts during evaluation, bounding space usage and preventing exponential growth in wrapper chains for tail-recursive programs.[36] Compile-time impacts in gradual typing focus on scalability for large codebases, where incremental type checking reuses prior results to avoid reanalyzing unchanged modules during iterative development or migration.[40] This approach supports gradual adoption by minimizing feedback loops in tools like Sorbet for Ruby or Pyright for Python. For integrating untyped libraries, type stubs provide static interfaces without runtime overhead, allowing typed code to scale by declaring expected types for library exports while deferring full checks to boundary contracts only when necessary.[41][42] Empirical measurements from 2015 to 2023 benchmarks, such as the GTP suite, reveal slowdowns of 10-50% (1.1-1.5×) in mixed programs for optimized systems like Pycket and Corpse Reviver, a marked improvement from earlier 20× overheads in 2016 due to wrapper optimizations and JIT advancements.[43] Recent 2024 research on discriminative typing introduces adaptive optimizations by inferring specialized function versions for common dynamic types, achieving up to 4× speedups over baselines like Reticulated Python by eliding unnecessary checks in typed-dominant configurations.[44]Examples and Case Studies
Typed Racket and Racket
Typed Racket is an embedded, gradually typed dialect of the Racket programming language, introduced in 2008 as Typed Scheme and later renamed, which enables the incremental addition of static type annotations to dynamically typed Racket code while ensuring type safety through contract-based enforcement.[45] This system treats untyped Racket modules as dynamically typed and inserts contracts at module boundaries to mediate interactions between typed and untyped components, allowing programs to mix both styles without requiring full rewrites.[46] The approach draws from contract systems in Racket to implement gradual typing semantics, where type errors are caught at runtime with precise blame assignment to the originating untyped code. Key features of Typed Racket include occurrence typing, which refines types based on runtime predicate checks to enable more precise static analysis, such as narrowing the type of a value after a conditional test like(if (number? x) ... ).[47] Blame tracking provides detailed error messages that pinpoint the source of type violations, assigning responsibility to either typed or untyped code while ensuring that well-typed typed code cannot be blamed for errors.[48] Additionally, it supports higher-order functions through dependent contracts and refinement types, accommodating Racket's functional programming idioms like first-class functions and continuations.[49]
In practice, Typed Racket programs begin with the declaration #lang typed/racket to activate the typed dialect, where users annotate functions, structures, and variables using a type syntax inspired by ML but extended for Racket's features. Mixing typed and untyped code occurs via forms like require/typed, which imports untyped identifiers with ascribed types, or by exporting typed values to untyped modules, where contracts are automatically generated to enforce gradual typing. Gradual migration is facilitated by tools such as the Typed Racket compatibility library and IDE integrations in DrRacket, which provide type checking, error reporting, and incremental refactoring support for transitioning legacy untyped codebases.
Typed Racket has served as a foundational platform for gradual typing research, influencing developments in sound type systems, performance optimization, and blame precision across multiple studies.[50] In education, it is widely used in programming languages courses to teach type systems and semantics, leveraging Racket's pedagogical tools. For production, it underpins applications like the Redex semantic modeling tool, where typed components ensure reliability in language design experiments, and supports real-world Racket-based systems in domains such as web servers and data analysis.
TypeScript and JavaScript Ecosystems
TypeScript, developed by Microsoft and first publicly released on October 1, 2012, is a superset of JavaScript that introduces optional static type annotations to enhance developer productivity and catch errors early during development.[51] It allows developers to gradually add types to existing JavaScript codebases without requiring a full rewrite, compiling to plain JavaScript that runs in any JavaScript environment.[52] Unlike sound gradual typing systems, TypeScript's type checking is unsound, meaning it may miss certain errors at compile time, particularly when interacting with untyped JavaScript, but it imposes no runtime overhead as type information is erased during compilation.[52] Key features of TypeScript include structural typing, where compatibility between types is determined by their shape rather than nominal declarations, enabling flexible object composition without explicit interfaces. It supports generics for creating reusable components that preserve type safety across different data types, as well as union types that allow variables to accept multiple possible types, such asstring | number. Additionally, definite assignment assertions, introduced in TypeScript 2.7, permit developers to assert that a variable will be assigned before use, helping to manage strict null checks in large codebases.
Within the JavaScript ecosystem, TypeScript integrates seamlessly with npm, the primary package manager, allowing installation via npm install -g typescript and the use of type definitions for third-party libraries through the DefinitelyTyped repository, which provides @types packages for over 30,000 modules. Tools like ESLint support gradual adoption by linting mixed TypeScript and JavaScript code, with plugins such as @typescript-eslint enforcing type-aware rules to maintain code quality during incremental migrations. As an alternative, Flow, released by Facebook on November 18, 2014, offers a static type checker for JavaScript with bidirectional type inference, which combines top-down checking from expected types and bottom-up inference from expressions to reduce annotation needs.[31][53]
TypeScript has seen widespread adoption in web development, powering frameworks like Angular, which is built entirely on TypeScript to leverage its type system for scalable single-page applications, and serving as the primary language for Visual Studio Code, Microsoft's code editor.[54] According to the 2023 State of JavaScript survey, approximately 41% of respondents used TypeScript for the majority of their projects, reflecting its role in large-scale projects for improved maintainability; by the 2024 survey, 67% reported writing more TypeScript than JavaScript.[55][56] However, challenges arise with third-party untyped libraries, often addressed via @ts-ignore directives to suppress type errors temporarily, though this can undermine type safety if overused; developers are encouraged to contribute or use community-provided declaration files instead.[57]
Other Languages and Tools
Hack, developed by Meta for the HHVM runtime, introduces gradual typing to PHP codebases, allowing seamless interoperation between dynamically and statically typed sections through features like strict mode for optional type enforcement.[6] Released in 2014, it supports incremental adoption by annotating existing PHP code with types without requiring full rewrites.[32] In C#, thedynamic keyword, introduced in version 4.0 with .NET Framework 4 in 2010, enables gradual typing by bypassing static type checks at compile time while leveraging the Dynamic Language Runtime (DLR) for runtime resolution. This facilitates dynamic behavior, such as with ExpandoObject for flexible property addition, and has seen runtime performance enhancements in .NET 8 through optimizations like dynamic profile-guided optimization (PGO).[58]
GDScript, the scripting language for the Godot game engine, adopted optional static typing in version 3.1 released in 2018, evolving into a gradually typed system that improves code reliability and editor support.[59] By Godot 4.0 in 2023, it matured with features like typed arrays, which enforce element types for better performance and error detection during development.[60]
Emerging gradual typing support in Python leverages tools like mypy and Pyright, which enable incremental type annotations on dynamically typed code without breaking existing functionality, aligning with Python's type hinting system introduced in PEP 484.[61] Mypy, an optional static type checker, supports gradual adoption by checking annotated subsets of codebases in the 2020s.[62] Similarly, Pyright provides fast, incremental type checking for large Python projects.
Tools like Pyre, an OCaml-based static type checker developed by Meta, further support gradual typing in Python by offering responsive, incremental analysis on massive codebases, catching type errors early while accommodating untyped legacy code.[63]