There are a few different approaches to gradual typing, and some have more impact on the language than others. One major cluster is sound gradual typing, which wants to ensure that the static type annotations are correct even when values from dynamically-typed code enters them. This requires dynamic checks and run-time errors.
Another major cluster only enforces types statically where present, and doesn't do anything with them at run time. These are especially common when adding static types onto an existing dynamically-typed language, as in TypeScript. Some people consider the term "gradual typing" to mean only sound gradual typing, and only refer to the others as "optional typing"; this can be a bit of a fight that I want to sidestep here.
If you're looking at one of the variants with no dynamic checking then the run-time effects are almost nil, but for strictly being "gradual" there are a couple of language-level impacts that do affect run-time behaviour, addressed below. The next section addresses only the sound systems with run-time checking, and we'll return to the language-design elements at the end.
There are several different strategies for run-time checks, trading off different implementation and behavioural traits. Three of particular note were identified by Vitousek et al. in Design and Evaluation of Gradual Typing for Python:
- The guarded semantics puts a proxy wrapper around a value when it passes from dynamically-typed code to statically-typed code. The statically-typed code only accesses the wrapper object, and the wrapper performs the type checking. Typed Racket follows this approach, but uses chaperones that avoid making the proxy detectable to code. Other references to the same object are not wrapped up, and it's unwrapped when handed back to untyped code.
- The transient semantics checks that the value's type is consistent with the static type when it passes through a type annotation, but doesn't hold on to that information for later. Further interactions that invalidate the type may go undetected, and there may be more checks than needed.
- The monotonic semantics permanently modifies the object itself to know what type it's expected to have. These modifications persist through the whole program and all references to that object, including ones that never went into static code, and including after the original static reference is obsolete.
Many variations on these are possible, and others. The ideal is to ensure that purely static code never has type errors, and run-time errors can be blamed on a piece of dynamically-typed code.
Making these run-time checks can be expensive, to the point of one system being described as "only 8,000 times slower", but these costs can be mitigated. The different enforcement strategies have different trade-offs, and so does the number of different type boundaries: the more places you have to check things, the more work there is, so allowing only whole modules to be typed or untyped can be easier than allowing toggling individual annotations.
The transient semantics is generally regarded as having the worst performance, with many redundant checks, though Roberts et al. showed that a high-performance optimising virtual machine could eliminate almost all of the cost of transient checks, and Richards et al. showed the same for the monotonic semantics. Optimisations of the chaperones in Typed Racket have produced very low overheads. All of these optimisations have real engineering costs and you should expect some level of slowdown, but it may well be manageable for the application domain of the language.
These optimisations can generally be implemented later on, but things like advanced chaperones are very invasive, so you'd probably want to commit to that approach early.
Language effects at run time
One defining point in Siek et al.'s Refined Criteria for Gradual Typing is the Gradual Guarantee: given a runnable gradually-typed program with at least one type annotation, removing a type annotation doesn't change the behaviour of the program.
The Gradual Guarantee imposes some unexpected restrictions on what other features the language can have that do have some run-time effects, even if the types aren't dynamically enforced.
In particular, the language can't have any features that cause the behaviour to change based on type annotations. That includes things like
instanceof or typecase pattern-matching with higher-order types:
x instanceof List is probably ok, but
x instanceof List<String> isn't, because removing the
String annotation would cause a different branch to be taken. In a structural type system, every type test is higher-order, so you can't have those features at all.
It also rules out some uses of constructs like Haskell's
newtype, where the annotated type is used to provide different behaviour or class instances.
On the other hand, nothing says you actually have to be gradually-typed under these complete criteria, so a language could include those features anyway; using the label is only a broad branding exercise anyway.