Hacker News new | ask | show | jobs
by ehutch79 877 days ago
A note; typescript does nothing to ensure types are correct at runtime. Especially in the browser. You need to do runtime checking even if you're using typescript.

In Python, yeah, they're called Type _Hints_ for a reason. Don't count on them at runtime here either.

Both are dynamic languages, it's hard doing anything meta/schema driven with rigid types. If you really want a hard type system, just move to GO or Rust or C or something with a real type system enforced.

4 comments

To be fair, TypeScript is a bit different because you can use it to compile to JavaScript rather than merely annotating in JavaScript files, which is crucial because it means that compilation can _fail_ to produce JavaScript. If I recall correctly, you can still have it produce JavaScript even if the type checking fails, but the fact that you can't just directly run the TypeScript makes a difference. With the Python type annotations, you can still just run the code directly without _ever_ compiling, let alone if type checking fails.

> If you really want a hard type system, just move to GO or Rust or C or something with a real type system enforced.

For what it's worth, Rust also has no types at runtime, which is why Rust doesn't have reflection and relies heavily on compile-time magic with macros. The difference is that there's no way to get a binary to run if the code doesn't typecheck. The most underrated feature of a compiler is that it can say "no"; in the case of Rust, the killer feature of the language isn't what you're allowed to do, but what you're _not_ allowed to do, even accidentally.

I'm not sure that's really a practical distinction between Typescript and Python. Most of the practical bundling setups I've seen use a tool like Babel or Esbuild, which doesn't do any type checking - instead the type checking is done as a linting/testing check beforehand, the same as with Python. And as you point out, even if you compile directly with Typescript, it will quite happily compile code that doesn't pass the type checks, as a configuration option.

And this isn't just a side effect of the build tools, but a genuinely useful feature. While I'm working on a change, I can run half-finished code and get some feedback before fixing the rest. This is particularly useful for tests - I can test a module, even if I've not yet updated all of that module's usages, to sanity check whether what I'm doing makes sense.

There can definitely be downsides to this separation of type system and runtime behaviour, but it's also very useful, and it works the same in Typescript and Python.

I think the biggest issue that Python's type checking has in comparison to Typescript is just sheer power: Typescript can very explicitly and correctly type real-world Javascript code, whereas typed Python, in my experience, ends up feeling a lot more like old-school Java than idiomatic Python. And it's difficult to sell old-school Java to Python developers.

> Most of the practical bundling setups I've seen use a tool like Babel or Esbuild, which doesn't do any type checking - instead the type checking is done as a linting/testing check beforehand, the same as with Python.

I think the difference is really one of UX (or DX I guess) and availability: Yes, if you already work with a toolchain, there won't be a difference. However, lots of people don't. If you just use the language runtime on its own and want to get your program running with the minimum possible effort, then for typescript, you'll run the transpiler and feed the resulting JS to a browser or nodejs vm; for python, you'll just run your program with the python interpreter directly.

The thing is that for the "minimum" typescript workflow, type checking is per default performed, while for the minimum python workflow it isn't. That you could also disable type checking for typescript or use a linter to get it done in python is besides the point - those are additional options and tools that you have to spend additional effort to activate - and you have to know about them in the first place. Someone who just has some basic skills in the language is unlikely to do so, so effectively, type checking for them is performed in typescript but not in python.

Ah, I see what you mean. I guess it's a matter of framing - to me, choosing to install and run Typescript is the equivalent of choosing to install and run mypy - in both cases, you've added an additional tool above and beyond the default (Javascript and Python, respectively). I guess I don't really see Typescript as a separate language from Javascript in the sense, it's just Javascript with knobs on.

So there isn't really a "minimum" Typescript workflow because by using Typescript, you're already choosing to install additional tools and set up a more complicated workflow (compile then run, as opposed to just running Javascript). And if you'd do that with Javascript, you'd do that with Typescript as well.

(I think it's no coincidence that the long-term plan from the Typescript team is to have type annotations become part of regular Javascript syntax, in the same way that Python includes type annotations out of the box. In both cases, they'd be ignored by the parser and only used as metadata for the purposes of a separate type checking tool. The Typescript team do not see themselves as developing a separate language to Javascript, but rather just a dialect that includes types.)

> A note; typescript does nothing to ensure types are correct at runtime. Especially in the browser. You need to do runtime checking even if you're using typescript.

I'm not sure I understand - in what situation would you need runtime type checking? I always assumed the situation was similar to Haskell where the types are erased at runtime but it's still impossible (absent opting in to unsafe stuff) to have type errors at runtime.

As the other commenter said, ingesting data from various sources. You don't actually know what's in that JSON.

There's also a myriad of ways to alter data running in a browser. From plugins to just fooling around with the console in DevTools.

On a large enough team, you might not be able to trust the code calling your code. It's easy to slip 'as whatever' in there. I do it when using vue's reactive objects instead of fighting with typescript.

Usually ingesting external data.

You NEED to verify data from outside your app anyways but if you have runtime checking, trying to cheat and skip that step is harder.

Python's type hints are great as machine-checkable statements about constraints on the behavior of your code. It's not strictly true that they're unavailable at runtime.

> Don't count on them at runtime here either

If you're adventurous enough, you can reflect on the type hints and check things yourself at runtime, but you have to understand that the type hints aren't meant for this and they could well blow up in your face. Still, I've had some success constructing dataclass instances from JSON objects based on what fields() tells me about the attribute types. Whether you want to do this yourself in production depends on your tolerance for hilarious edge cases and interpreters that get to do things differently because nobody promised you anything.

> It's not strictly true that they're unavailable at runtime.

So is it possible for me to enforce type hints at runtime, so my program crashes if a type is ever wrong? How do I turn this checking on?

If you want to. It would be a lot of work. I check input data on the boundary and rely on mypy to tell me when I've made a type error in the interior, and I find that works well enough. Also, see my disclaimers, this stuff isn't really meant to be used how I'm using it; it's meant for static checkers like mypy and I'm just abusing the fact that the interpreter does anything at all with type hints. But if you really wanted to, you could probably write a decorater that would do the same kind of checking I'm doing at the boundary.
Not exactly, but if you use something like Pydantic, you can check data at the boundaries of your application/library. If the data passes through the pydantic model, it's safe.

Add static type checking of your code through mypy or pyright, and now you can reasonably guarantee that your code is type safe, and stuff interacting with your code is type safe.

Not perfect, but it's a reasonable approach for important libraries!

It's not practical as a default, but yes.

See for example https://pypi.org/project/py-strict-typing/

It's possible to introspect type hints at runtime, in the same way you can inspect e.g. a function's name or parameters. However, there is no built in way of using these type hints as a runtime-enforced check. There are a handful of libraries that can do some introspection and runtime checking, but they typically come with a significant performance cost.

That said, using these sorts of tools (or trying to enable runtime type checks in general) is usually a misunderstanding of how the type checking works. Think of type annotations less as a way to enforce types, and more as a way to explicitly declare intention. If you annotate a function as taking a string and returning an int, then you're saying that, if the caller obeys their side of the deal and passes you a string, then you'll obey your side of the deal and return an integer. Moreover, a type checker can demonstrate that this is the case: that for every path through your function, assuming the input is always a string, then you will return an integer. This frees you from runtime checks entirely, because the checks can happen entirely at compile time: assuming the input types are correct, your code will return values of the correct type in return. If your whole program is type checked, then we can be confident that the whole program behaves correctly in terms of types.

There are two big exceptions to this. Firstly, runtime data cannot be checked in this way, so needs to be checked at runtime. Usually this happens at very explicit boundaries: you load a TOML file, and use Pydantic to validate the file and parse it into a Python structure. If the Pydantic check succeeded, then you know the data had the correct types, and therefore that your function will behave correctly as outlined above. If it failed, you get a runtime error at the exact point that the invalid data enters your system. The danger is doing something like json.loads and then not validating the result in any way.

The second exception is when you manually choose to ignore the type system and tell it what's going on. This includes type assertions where you override Mypy's expectations for what a type should be, or the use of the `Any` type. In these situations, you deliberately can the compact you've made with the type checker, but you also get a chance to handle cases of extreme dynamism in Python that the type checker can't deal with. These sections of your code will naturally be unsafe, but are usually more explicit and so can be checked more carefully in code review.

The result is that, with a type system that handles this stuff well, you do not need to enforce type hints at runtime (and trying to enforce type hints at runtime is usually a sign of not leaning sufficiently into the type system in the first place). That said, "a type system that handles this stuff well" is the key phrase here - Typescript is a good example of a tool that works well as a static-only type checker, in that it gives you a lot of power to define types that match the underlying runtime. In my experience, there isn't yet a tool powerful enough to handle that in Python, which can make developing with type hints quite frustrating at times. But I don't think runtime type checking would change that at all.

C's type checker is the MMU - when you mess up types, it give you a segfault. If you're lucky.
Funny, but for the readers who don't know C -- this isn't true. C has type checking at compile time.
It has something, but not what you'd call modern type checking:

  #include <stdio.h>

  int main(void) {
    unsigned int positive_number = -1;
    printf("%d", positive_number);
    return 0;
  }
Prints -1.
I had to check this because my intuition told me that this would generate a warning. GCC and Clang both warn about that assignment with -Wsign-conversion, however that doesn't seem to be enabled with any of -Wall, -Wextra, or -Wpendatic and only clang's -Weverthing would catch it if you weren't specifically looking for it.
that's because assigning -1 to an unsigned number is an idiomatic, safe and portable way to setting all bits to 1. You could argue that an explicit cast should be warranted in this case, and I sort of agree, but it would break way too much code.
I must be missing something here. Wouldn't an unsigned int be unable to represent -1 since the sign bit is part of the value?
C implicitly converts integer types as needed. The specific conversion rules are rather convoluted, see section 6.3 of the ISO C standard for the details. Before C23 this was implementation defined, but since C23 now mandates two's complement representation of signed integers and wrapping for unsigned overflow (as before) the `-1` is converted to UINT_MAX. Then the `%d` format specifier performs a conversion from `unsigned int` back to `int` for printing, resulting in `-1`. Implicit conversions between types of the same rank are guaranteed to "round-trip", you get back the value you started with when converting back to the starting type. If they'd specified an unsigned format specifier for printing then they wouldn't get `-1` printed.
I'm pretty sure that this was well defined and portable well before C23. Although the representation was implementation defined, IIRC signed to unsigned has always been guaranteed to be as-if in two-complement.
The -1 here is hiding the fact that -1 is really just 0xffffffff due to two's complement (architecture dependant of course)

And printing it as %d is technically a misuse of printf, since %d means print as a signed integer. If you did %u (print as unsigned integer) instead you'd see the value is really 4,294,967,295 (again, platform dependant)

I knew someone would say this :) But my point is that how can I assign a negative number to an unsigned int in the first place?
Fair.