Hybrid typing synthax
Brings static typing alongside of dynamic typing.
Types as comments will break forward compatibility and close forever future type language feature.
Extending aspects of Type annotations proposal.
Best chance to improved dev experience and last chance to add types to ecmascript.
Key points
Effortless
- You don't have to type if you don't want.
- Imported code will (if typed) cast value for you and only bring you transparent safety and performance.
Costless
- Declarations and typed code reduce memory footprint and engine abstractions/instructions during runtime.
- Parse time is reduced as type error will fail fast (before runtime for explicit mismatch).
- Bundler could remove types if you focused on size and keep some file typed for performance and runtime safety.
Progressive
- You can type some parts without affecting the rest of your codebase.
- TC39 type system functionnalities can be extanded progressively by proposals, only some core functionnality is needed to start.
Granular
- Only some specific performance sensite parts (eg: complexe computation) or type safety (eg: assert fetching json) can be type.
Wasm
- Better compatibility with wasm and maybe future interoperability with wasm component model.
If type annotations evolved to hybrid typing runtime
- Don't break backward compatibility.
- Zero cost and dev friendely.
- The developper choose, use type system or not (interroperability).
- Bring security and performances.
- Can be directly optimized/compiled by engine, no need for JSObject or JSValue (V8) with type testing and deoptimizations.
- One of the most requested features with no effect for js developper who don't wan't typing.
- Embeded architectures (arduino, esp, ...).
- High performance code.
- Reduce memory footprint (less typechecking in internal compiled code).
All legacy typed JS work and is default behaviour (no fear for vanilla js lovers)
New possibilities for type lovers
Supersets like typescript and flow can transpile to "static typed" js
Rules
- All legacy and no-typed variables are considered as any (actual behaviour)
any
type is dynamic. - Explicit typed code is statically typed and can perform checks before runtime.
- Explicit typed variables are castable to any without
any
restriction. - Cast (not coercion) from
any
to typed is runtime asserted for objects and before for primitives. - Types assertion (eg:
as
keyword in typescript):- Perform runtime type cast until mismatch and then raise a TypeError for
any
. - Typed code is allowed to perform check berfore runtime.
- Perform runtime type cast until mismatch and then raise a TypeError for
- Explicit cast needed for no statically analysable code
const any = 5 const count: number = any //Ok, implicit cast because statically analysable const users: User[] = [] const apiResponse = await fetch('https://api.example.com/users') users.push(await apiResponse.json()) //Error, can't analyse apiResponse type users.push(await apiResponse.json() as Users[]) //Ok, cast is explicitely write
Futher suggestions
- Types as firstclass primitives eg:
typedef
- New Global object Type to add new specific type operations (because introducing new operators can be tricky)
interface Type { is(value: any, type: typedef) => boolean of(value: any) => typedef //structural typedef compare(type1: typedef, type2: typedef) => 'equal' | 'subset' | 'superset' }
- Narrowed primitives
Some engines can implementes their own subset based on existing types (type aliased in other environnement), for instance for IOT with types likei32, u32, ...
i8, u8, ...
Examples
(see Typescript use cases and Rust to imagine the revolution)
Declaration
let text = 'some text'
let count: number = 5
text = 5 //Ok
count = 'some text' //Runtime (or before if aviable) error
Equivalent to strongly typed code
let text: any = 'some text'
let count: number = 5
text = 5 //Ok
count = 'some text' //Runtime error
Implicit cast
const users: User[] = []
const response = await fetch('https://api.example.com/users')
users.push(await response.json()) //Runtime throw if cast error
Explicit cast
const response = await fetch('https://api.example.com/users')
const users = await response.json() as User[] //Runtime throw if cast error
//Or
const users: User[] = await response.json()
//Or
const users = await response.json::<User[]>() //synthax from type annotations proposal examples
//Runtime throw if cast error
const anyTyped = '5'
foo(anyTyped as number) // Error, cast is not coercion
Generics
function copyEntry<T extends Entry>(entry: T): T {
return new T({... entry})
}
const user1 = new UserEntry(/*...*/)
const user2 = copyEntry(user1)
//functionnal programming
class None {
unwrap() {
throw new Error('none value')
}
}
class Some<T> {
#value: T
constructor(value: T) {
this.#value = value
}
unwrap(): T {
return this.#value
}
}
type Option<T> = Some<T> | None
const value: Option<number> = None
const value2: Option<number> = Some(5)
const value3: Option<number> = Some('') //TypeError (before runtime is aviable)
const neverTypeError: number = value2.unwrap()
New use cases
type Api = Type.of(await (await fetch('https://api.example.com/type_format')).json())
function reshape<T>(payload/* no type = any */): T {
/* ... */
}
const payload = /* ... */
await fetch('https://api.example.com/', {
method: 'POST',
body: JSON.stringify(reshape<Api>(payload))
})
Optimisation issues
-
Declaration
let count: number
V8 inspired pseudo code
//actual JSValue count; //new f64 count;
-
Functions
function mean(a: number, b: number): number { return (a + b) / 2 } const result: number = mean(2, 4)
V8 inspired pseudo code
//actual no-opt JSFunction mean(JSValue a, JSValue b) { return JSAdd(a, b) / 2; } JSValue result = mean(2, 4); //actual opt JSFunction mean(JSValue a, JSValue b) { if (JSValueIsNumber(a) && JSValueIsNumber(b)) return (a + b) / 2; deoptimize() } f64 result = mean(2, 4); //new directely opt f64 mean(f64 a, f64 b) { return (a + b) / 2; } f64 result = mean(2, 4);