Object prototypes is a weird part of JS that we really could do without. If you really want that, use class syntax.
The Date object. Need I say more...
Little things like Object.keys() should return a (keyof T)[] rather than a string[] but can't due to JS edge cases.
Want first class tuples/immutable arrays.
Many other new syntaxes that can't be implemented due to the need for JS compatibility.
I'm sure there are others that I can't think of right now...
I think we can all agree the Date API sucks, but life can still be good if you just give in and include a date library. Also, there's some light at the end of the tunnel with Temporal proposal coming along [1].
Object.keys returning string[] is purely a TypeScript design decision, coming from how TypeScript chooses to model object subtyping [2].
type Foo = {
a: string
}
const keysOfFoo = (obj: Foo) => Object.keys(obj)
const foo = {a: '', b: 5}
keysOfFoo(foo)
The fact that this passes type checks is a conscious TS design decision that comes with both advantages and disadvantages. It wouldn't have to be this way; Exact types [3] could potentially be used to describe that if the type is exactly Foo, it's safe to assume Object.keys(foo) is (keyof Foo)[].For first class tuples (and records), there's a stage 2 EcmaScript proposal coming along [4]. There's of course also the readonly [] type in TypeScript if you only need the safety of not accidentally pushing to an "immutable" array. First class language support could have nice additional features though, like strict equality.
Anyways, if these are the things you don't like about JS/TS, I don't think AssemblyScript will be the answer for you, as the goals of that language seem to be entirely different from "fixing" old JS cruft. Apart from the i32, i64 stuff I guess.
[1] https://github.com/tc39/proposal-temporal
[2] https://github.com/Microsoft/TypeScript/pull/12253#issuecomm...
Runtime types can still get you there. I’ve been happy with myzod for this purpose, which feels like writing TypeScript while getting you everything you need to validate at runtime in a very performant package.
Basically AssemblyScript try to fix JS runtime as well. See this: https://twitter.com/AssemblyScript/status/114604718874680934... and https://twitter.com/AssemblyScript/status/114466636945316659...
On this subject specifically, see the records & tuples proposal (currently at stage 2): https://github.com/tc39/proposal-record-tuple
Also, the array access not returning an optional type (I wish there was a strict option for that) often gets me.
We need a middle ground: “TypeStrict“ that can clean up some of the edge cases.
If you need strict integers, you can create your own fake type:
type Int32 = number & {__type:'Int32'};
I do this with strings sometime when I want a string subtype that can’t be accidentally assigned by a normal string.
https://devblogs.microsoft.com/typescript/announcing-typescr...
[1]: https://devblogs.microsoft.com/typescript/announcing-typescr...
I was just searching for that and all I could find were old github issues saying it was working as intended.
Thank you!
This was something that confused me when I first started learning Typescript but is actually not an edge case but a fundamental feature of Typescript's structural typing.
A type T is only guaranteed to be at least (a subtype of) T. Which in the case of objects means it has at least those fields, but potentially more. For instance
interface FooBar {
foo: string
bar: string
}
interface Baz {
baz: number
}
const fooBarBaz: FooBar & Baz = {
foo: 'foo',
bar: 'bar',
baz: 3
}
const printFoobar = (fooBar: FooBar) => {
for (const key of Object.keys(foobar)) {
if (key !== 'foo' && key !== 'bar') {
// If Object.keys was (keyof T)[] Typescript would claim this branch is unreachable
}
}
}
printFooBar(fooBarBaz)
It's really worth thinking about Typescript types not as nominal objects like in Java or Haskell, but contracts about minimum functionality. Embracing this in terms of function arguments and return types makes testing and composing typescript code much easier. For example, if you were writing a AWS Lambda function in Typescript that only uses the body of the incoming event. export const handlerOne = (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
console.log(event.body)
return {
statusCode: 200,
body: event.body,
}
}
interface MinimalEvent extends Pick<APIGatewayProxyEvent, 'body'> {}
export const handlerTwo = (event: MinimalEvent): Promise<APIGatewayProxyResult> => {
console.log(event.body)
return {
statusCode: 200,
body: event.body,
}
}
handlerTwo only requires a handlerTwo({ body: '....' }) call in a test, instead of having to fill in all the extra properties of the APIGatewayProxyEvent that aren't even required. You might be tempted to use a type coercion in the test instead e.g. handlerOne({ body: '....' } as APIGatewayProxyEvent) but the problem with the coercion is that is in not checked at all by the compiler, it's essentially like temporarily using an any type. So if handlerOne is updated in the future to depend on more of the structure of APIGatewayProxyEvent Typescript will be none the wiser that the test requires updating (although granted hopefully the test would fail).Anyway, that's a long winded explanation of why Object.keys shouldn't return (keyof T)[]. If you want to program generically over the Type of something I'd suggest making use of a runtime type library like io-ts instead which gives you a data structure representing the type to iterate over, etc.
const x = { a: 1, b: 2, c: 3 }
const y: { a: number, b: number } = x
console.log(Object.keys(y))Your requirements list makes me wonder why you want to stick with anything related to JS at all? With all the transpiling options, web assembly, etc.
I'm thinking of OCaml for example. Why not use OCaml rather than TS?
Edit: Actually the idea is so obvious that you have ReasonML that can compile both to JS and assembly and is basically OCaml with a JS-like syntax: https://reasonml.github.io/docs/en/what-and-why
Isn't that exactly what you're thinking of?
But I would love runtime checks for types. Even if that was a compiler step I could flag on—but that makes it a great deal more complex both to implement and reason about, likely—without starting fresh anyway.
const shout = (message: string) => console.log(message)
into something like
const assert = require('assert'); const shout = (message) => { assert(typeof message === "string"); console.log(message); }
of course this doesn't work for more complicated types
IMO, runtime type analysis is a code smell. There are few scenarios where the simpler and more efficient solution involves querying type metadata at runtime. Particularly in a language that directly supports dynamic dispatch, you should think hard before adding anything resembling an "if typeof(mything)" check.
(Reading through the thread, there’s apparently some libraries that can do this; I’ll have to try them out next time I need this.)
See for example Tetris implemented in Pokémon Yellow via runtime code injection:
https://www.youtube.com/watch?v=Vjm8P8utT5g
Their compiler missed that!
So I think there's good reason to switch from talking about types as "assumptions", and think of them as compile time "assertions". Something lower-level in the stack could break the high-level code, but that doesn't make the high-level code useless, just imperfect.
Types can and do affect the way programs run at the lowest level. Perhaps someone with a better understanding of compiler theory can elaborate further on this...