Safe & Efficient Gradual Typing for TypeScript Aseem Rastogi University of Maryland, College Park Nikhil Swamy Cédric Fournet (Microsoft Research) Gavin Bierman (Oracle) Panagiotis Vekris (UCSD) Gradual Typing Combines benefits of static and dynamic typing Static type errors Performance Typed interface documentation Dynamic type any Statically-typed fragment Dynamically-typed fragment Rapid prototyping Flexibility Runtime checks mediate interaction 2 Increasing Adoption in Industry Mainly for JavaScript TypeScript, Dart, Closure, Flow (we focus on TypeScript) Increase programmer productivity (static type errors, intellisense, code refactoring, modularity …) Types don’t get in the way of good programmers (retain flavor of programming in JavaScript) 3 TypeScript is Intentionally Unsound For All Its Dynamic Idioms, Typing JavaScript is Hard ! Unsound typing rules to support supposedly common idioms (covariant array subtyping, covariant function arguments, …) Types are uniformly erased during compilation (lightweight compilation, no runtime performance cost, …) (unchecked runtime casts, types don’t guarantee anything) 4 Programmers Cannot Rely On Types private parseColorCode (c:string) { if (typeof c !== "string") return -1; … } Snippet from TouchDevelop, a large TypeScript Development Tools can’t rely on types Refactoring, compiler optimizations, etc. are not safe 5 Can we design a gradual type system that is: Safe Efficient Supports idiomatic TypeScript/JavaScript We present Safe TypeScript 6 Safe TypeScript is Sound TypeScript is Intentionally Unsound Standard variance for arrays and function arguments Sound treatment of JavaScript this Careful separation of nominal and structural types Easy local rewriting of unsound idioms in 120,000 line corpus TypeScript has unsound typing rules to support common idioms 7 Safe TypeScript is Sound TypeScript is Intentionally Unsound Safe TypeScript guarantees type safety Combination of static checking and runtime checks Runtime-type-information (RTTI) based gradual typing Efficiency using two new notions of partial erasure Runtime overhead of only 15% for bootstrapping Safe TypeScript 6.5% for typed Octane benchmarks TypeScript uniformly erases all types making uses of any unsafe 8 Our Contributions Formalized core of Safe TypeScript, proven sound class C, interface I, {M;F}, number, any, Erased t Implemented in a branch of TypeScript v0.9.5 Can be invoked with --safe command line flag Evaluated on 120,000 lines of code Bootstrapped Safe TypeScript compiler, New TypeScript compiler, Octane 9 Safe TypeScript Tour Overview of RTTI-based Gradual Typing Values carry Run Time Type Tags consistent with their contents f = 2 g = "s" h = true tag = {f:number; g:string} Object with 3 fields and type tag Dynamically-typed code instrumented to respect RTTI tags Invariant: v : [| v.tag |] 10 RTTI Tagging in Safe TypeScript On-demand and Differential interface Point { x:number function g(p:Point) { p.x = p.x + 1; } function f() { var p:Point = { x = 0 }; g(p); } Safe TypeScript } Compiles unchanged No runtime checks No tagging ! Previous systems tag eagerly JavaScript 11 RTTI Tagging in Safe TypeScript On-demand function g(p:any) { p.x = p.x + 1; } function f() { var p:Point = { x = 0 }; g(p); } Add RTTI when types lose precision function f() { var p = { x = 0 }; g(shallowTag(p, Point)); } shallowTag(x,t) = x.tag := combine(x.tag, t); x Safe TypeScript JavaScript 12 RTTI Tagging in Safe TypeScript Differential interface 2dPoint extends Point { y:number } Add Minimum Required RTTI function h(p:any) { .. } shallowTag(x,t) = x.tag := combine(x.tag, t); x function g(p:Point) { h(p); } function g(p) { h(shallowTag(p, Point)); } function f() { var p:2dPoint = { x = 0; y = 0 }; g(p); } function f() { var p = { x = 0; y = 0 }; g(shallowTag(p, { y:number })); } Safe TypeScript JavaScript 13 RTTI Tagging in Safe TypeScript Add Minimum Required RTTI shallowTag(x,t) = x.tag := combine(x.tag, t); x function g(p) { h(shallowTag(p, Point)); } function f() { var p = { x = 0; y = 0 }; g(shallowTag(p, { y:number })); } x = 0 y = 0 JavaScript 14 RTTI Tagging in Safe TypeScript Add Minimum Required RTTI shallowTag(x,t) = x.tag := combine(x.tag, t); x x = 0 y = 0 tag = {y:number} function g(p) { h(shallowTag(p, Point)); } function f() { var p = { x = 0; y = 0 }; g(shallowTag(p, { y:number })); } x = 0 y = 0 JavaScript 15 RTTI Tagging in Safe TypeScript x = 0 y = 0 tag = {x:number; y:number} x = 0 y = 0 tag = {y:number} Add Minimum Required RTTI shallowTag(x,t) = x.tag := combine(x.tag, t); x function g(p) { h(shallowTag(p, Point)); } function f() { var p = { x = 0; y = 0 }; g(shallowTag(p, { y:number })); } x = 0 y = 0 JavaScript 16 Differential Subtyping Technical Device for On-Demand and Differential Tagging Differential Subtyping t1 <: t2 ~> d d = 0 | t d is loss in precision that must be captured in the RTTI 17 Differential Subtyping t1 <: t2 ~> d d is loss in precision t <: t ~> 0 {x:number;y:number} <: {x:number} ~> {y:number} {x:number;y:number} <: any ~> {x:number;y:number} Primitive RTTI Aware: number <: any ~> 0 18 Differential Subtyping t1 <: t2 ~> d d is loss in precision Γ |- e : t2 ~> e’ t2 <: t1 ~> d T-Sub Γ |- e : t1 ~> shallowTag(e’, d) 19 Instrumentation of Dynamically Typed Code function g(p:any) { p.x = "boom"; } function f() { var p:Point = { x = 0 }; g(p); assert(typeof p.x === "number"); } function g(p) { write(p, "x", "boom"); // Fails } function f() { .. } write(o,f,v) = let t = o.tag; o[f] = check(v, t[f]); (Recall that f tags p with type Point) Safe TypeScript JavaScript 20 Differential Tagging Useful, But Not Ideal function g(p:Point) { return p.x; } function g(p) { return p.x; } Unnecessary shallowTag function f() { var p:2dPoint = { x = 0; y = 0 }; g(p); } Safe TypeScript function f(p) { var p = { x = 0; y = 0 }; g(shallowTag(p, { y:number })); } JavaScript 21 Reason: Need Conservative Tagging Previous gradual type systems: t any for all static types t Incurs tagging even for statically typed code Relaxing it opens opportunities for sound type erasure 22 Erased Types Programmer-controlled Tagging Behavior A new type modality: Erased t Erased t cannot be cast to any Statically a t but may not have an RTTI at runtime 23 Subtyping for Erased Types t1 <: t2 ~> d t1 <: Erased t2 ~> 0 Erased t <\: any // Zero delta // Ensure full erasure is safe 24 Erased Types Example: No Tagging function g(p:Point) { return p.x; } function g(p) { return p.x; } Unnecessary shallowTag function f() { var p:2dPoint = { x = 0; y = 0 }; g(p); } Safe TypeScript function f(p) { var p = { x = 0; y = 0 }; g(shallowTag(p, { y:number })); } JavaScript 25 Erased Types Example: No Tagging function g(p:Erased Point) { return p.x; } function g(p) { return p.x; } function f() { var p:2dPoint = { x = 0; y = 0 }; g(p); } function f(p) { var p = { x = 0; y = 0 }; g(p); // No tagging } Safe TypeScript Recall shallowTag for non-erased types JavaScript 26 Erased Types Example: No Tagging function h(p:any) { .. } function g(p:Erased Point) { return h(p); } function f() { var p:2dPoint = { x = 0; y = 0 }; g(p); } Safe TypeScript Static Type Error function f(p) { var p = { x = 0; y = 0 }; g(p); // No tagging } JavaScript 27 Erased Types Obey a fully static type discipline (Sound type erasure, type abstraction, …) (Loss in expressiveness: not all code can be turned dynamic) Other advantages of Erased types Working with external libraries that may not have RTTI Adding new features to the type system (e.g. Polymorphism) See our paper for more details 28 Soundness Theorem: Forward Simulation Checks introduced by Safe TypeScript: Catch any dynamic type error Do not alter the semantics of type safe code Tag heap evolution invariant 29 Safe TypeScript Implementation Open Source on Github Implemented in a branch of TypeScript v0.9.5 Invoke our type checker with --safe flag 10,000 Lines of Code Compiles to plain JavaScript (with instrumented checks) (recursive interfaces, inheritance, overloading, generics, arrays, external libs, …) 30 Bootstrapping Safe TypeScript 90,000 Lines of Code, heavily class based, carefully annotated Number of Errors Covariant method arguments 130 Variable scoping 128 Potential unsound use of this 52 Bivariant array subtyping 98 ... … Total 478 Static Type Errors found in Safe TypeScript Code Bootstrapping Safe TypeScript 90,000 Lines of Code, heavily class based, carefully annotated All cases in one file using visitor pattern, easily rewritten Sloppy use of var declarations, easily fixed Projection of methods, easily fixed by making them functions Fixed by introducing array mutability qualifiers Number of Errors Covariant method arguments 130 Variable scoping 128 Potential unsound use of this 52 Bivariant array subtyping 98 ... … Total 478 Static Type Errors found in Safe TypeScript Code Bootstrapping Safe TypeScript 90,000 Lines of Code, heavily class based, carefully annotated 478 Static type errors 26 failed runtime downcasts; 5 in our own code ! 15% runtime overhead of type safety Octane Benchmarks (6 / 17) 22x (ave.) and 72x (max.) overhead with no type annotations Down to 6.5% after adding type annotations No overhead for statically-typed code Found a variable scoping bug in navier-stokes It has since been fixed Efficiency conditioned on precise type inference TypeScript quite conservative Also in the paper … Nominal handling of prototype-based classes Sound handling of this Abstraction theorem for Erased { } Zero-subtyping to avoid object identity issues More experiments (TypeScript v1.1, Octane benchmarks, Different tagging schemes) 35 Safe TypeScript Summary Safe & Efficient Gradual Typing for TypeScript Significant value for type annotations at a modest cost (runtime checks at least during development and testing) When to use Safe TypeScript ? For mature TypeScript projects, improved code quality Good in development setting for finding early bugs Baseline for JS analyses focusing on deeper properties Give it a go ! 36 Erased Types Example: Type Abstraction interface Point {..} interface 2dPoint {..} function g(p:Point) { (<any> p).y = 3; } function g(p:Point) { write(p, "y", 3); } function f() { var p:2dPoint = {x = 0;y = 0}; g(p); } function f() {..} RTTI of p at write is {y:number} write succeeds: abstraction violation Safe TypeScript JavaScript 37 Erased Types Example: Type Abstraction interface Point extends Erased {..} interface 2dPoint extends Point {..} function g(p:Point) { (<any> p).y = 3; } Static Type Error function f() { var p:2dPoint = {x = 0;y = 0}; g(p); } Safe TypeScript JavaScript 38