Hacker News new | ask | show | jobs
by 91edec 1612 days ago
I started a project with a Deno backend and Svelte front end last week and it was painful trying to share types between the projects. Deno not allowing imports without file extensions '.ts' and the Typescript tsserver not allowing imports with a '.ts' extension is really annoying, and neither group seems to want to budge. I ended up having to create a shared types folder and a script that copies the files to each project stripping out file extensions for non-Deno projects.

Does anyone else have a better solution shared Typescript code?

10 comments

Deno has _very_ custom module resolution rules. The first thing you notice is the required `.ts` extensions and browser-esque URLs. Oh, and also the import maps proposal (which is its own can of worms). Then you dig a little deeper and learn that deno also supposedly supports all `node` resolution rules sometimes via compat flags. Then you learn that it pulls types from magic `/// <reference types` comments, which sounds like what TS normally does, until you realize deno uses it to _override_ the types for JS files, and not pull in new files, which isn't something TS does at all. Oh, and `@deno-types` comments for overriding individual imports, besides.

One of the first lines in the deno docs is:

> Deno has no "magical" module resolution.

Which, as an implementor of module system rules, seems incredibly far from the truth of the matter (where deno has the most complex resolution rules of any runtime currently in use). I think maybe there were simple goals at the start, but that came crashing against reality and xkcd 927 executed in full force.

I would argue it isn't very custom. Using fully qualified URLs predates Node.js and CommonJS. It was also one of the big debates when we all were doing AMD. Node.js has realised that _mistake_ of eliding extensions and is headed that direction as well.

We have been adding a Node.js compatibility mode, but I am not sure that qualifies as "_very_ custom module resolution".

Both the triple-slash reference and @deno-types were solutions to problems to not have opaque type resolution with TypeScript. They do not impact the runtime resolution, only the type check resolution. Instead of opaquely searching for these by reading the `package.json` and probing the file system like `tsc`, Deno is explicit.

I would be glad to work with you to explore how we can figure out how to resolve your implementor concerns. The door has been open for a long time, and I have mentioned it in passing to Daniel a couple times. Let me know if you would like to talk.

this is web dev mindset, in all other languages the module name is just a name, it's not supposed to tell you how to retrieve the module, that's the job of the module system. Otherwise it's the service locator anti pattern. I always find it redundant that syntacically, the module name in javascript has to be a string (as in quoted) for whatever reason. In python and php it's just tokens and namespace separator.
This also makes using TypeScript libraries that weren’t authored specifically for Deno nontrivial.

One solution is to use an import map on the Deno side to map your extensionless imports to the corresponding files, but this only works when you control the Deno command line arguments (so not for libraries you plan to publish for others to use).

It’s unfortunate because, other than this huge problem, Deno is the best TypeScript runtime I’ve found.

> Deno is the best TypeScript runtime I’ve found.

I may be nitpicking here, but Deno isn't a typescript runtime, is it? In the usual case, does it not transpile typescript to JavaScript and run the code from there?

Yes, but I'd argue that is an implementation detail. You can directly run a TypeScript file like this `deno run helloWorld.ts`.

Is Node really a JavaScript runtime? V8 converts the JS to an AST. Would that make Node an `AST runtime`?

> Yes, but I'd argue that is an implementation detail. You can directly run a TypeScript file like this `deno run helloWorld.ts`.

Why yes, you can run directly Typescript code in Node (well, at least just as direct as Deno, as it transpiles TS to JS) with the likes of ts-node.

https://www.npmjs.com/package/ts-node

Then it would be fair to say ts-node is a typescript runtime, while node is a javascript runtime.
Another good workaround is to use something like JSPM, which bundles up the NPM library with all its dependencies into an ES module, ready to be consumed (and even loaded remotely!) by Deno
> I ended up having to create a shared types folder and a script that copies the files to each project stripping out file extensions for non-Deno projects.

Still a hack, but perhaps symlinks could be a little simpler?

I get the appeal of TS, but why the fuck would any project tie themselves to the language in this way? Angular 2+ did it, I didn't realize Deno did as well. Very bizarre decision unless I'm missing something.
What do you mean? You can use TypeScript or JavaScript, so .ts or .js extensions.

I think what you are missing is that, if you use TypeScript in Deno, the extension is .ts. What's confusing is the way Node.js+TypeScript does it, which means using TypeScript with .js extensions.

Ohh I see. I should probably delete my comment then. I thought the OP meant that Deno enforces the use of Typescript like Angular does. That makes a lot more sense, thanks for the explanation.
The way TypeScript/Node does it is correct. The thing you're importing is JS with a .js extension. If you publish to npm you're publishing the JS files, not TS files. If you write .d.ts/.js pairs instead of .ts - which should be identical to importers, there's no .ts file to import.
Considering that Ryan Dahl started both Node.js (where imports do not include file extensions) and Deno (where he added them back after deciding it was a bad decision to leave them off) I'm not sure how you've come to the conclusion that TypeScript/Node does importing correctly. Additionally, the ecmascript import syntax https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... specifically mentions that your tooling may or may not need file extensions but the examples in the spec (and indeed in browsers at large) require a full or relative path including the extension. If anything, I'd bet that file extensions make it into Node and tsc in the next few years.
Node requires the extension for imports already, unless the package you're importing from has defined extensionless entries in an export map.
I don't see how it is correct. If you publish to npm, of course you do it the way Node does it, but that's not very interesting. If you publish a module for consumption by Deno, you're publishing TS files, not JS files, and there is no need for .d.ts files.
Just like the sibling comment stated. Angular does not force you to use Typescript, you can use JavaScript. Angular 2+ even used to work with Dart but they dropped it.
> Angular 2+ even used to work with Dart but they dropped it.

"They" dropped Angular altogether: https://blog.angular.io/finding-a-path-forward-with-angularj...

I was not referring to Angularjs. I specifically mentioned Angular 2+.
My bad. Didn't know there was a "fork"... Thanks.
Deno uses SWC under the hood, so a large chunk of its TypeScript support is already coming from upstream.
Another question, how are people actually using Typescript with Svelte? It seems like you can't use $some.prop = avalue, it can't be type checked, so wtf is the point of using Typescript with Svelte?
Having used Typescript with Svelte, I think that Typescript is still potentially useful, just not directly in `.svelte` files. There's nothing wrong with keeping all your business logic in separate `.ts` files and then importing them into your Svelte components, because those components can and often are primarily view logic, and the utility of having everything type-checked down to the template level is... not particularly significant IMO. If your Typescript modules are still type-checked and pass tests, then I find trying to make a Svelte project Typescript all the way down to be kind of paranoid.

To each their own, but I think life's too short to be adding types to your view logic and your templates. It adds so much complexity for so little gain. Maybe it'll get better at some point where it will change my mind, in particular when using something like Deno.

I'm not sure about "$some.prop = avalue", but props are declared as exports of your component [1], and they can be given types. Perhaps you're refering to "$$props"? The only time I've hacked into the component variables is to check whether slot is set.

[1]: https://svelte.dev/tutorial/declaring-props

Damn I was hoping Svelte had proper Typescript support given that they claim that Svelte officially supports TypeScript. Is it just like Vue - it "supports" it, but there's still important stuff that's not type checked at compile time?
Well, good news for you, it does! Very well, in fact. Svelte itself is written in TypeScript.
So is he wrong about not being able to type check that then?
Well, yes, to my understanding. If you add lang="ts" to the script block, you can write normal TypeScript code for the component logic, and it will also check props that you are passing to child components in the template. It's like renaming the file from ".jsx" to ".tsx". It might not be perfect, but I haven't encountered any problems with it so far.
Vue 3 is written in typescript and does type check props at both compile time and runtime.
Not very nicely: https://v3.vuejs.org/guide/typescript-support.html#annotatin...

Does it type check templates yet?

Nobody should still use the awful legacy vue component syntax, it is trivial and much cleaner to migrate to script setup components, which do provide properly type checked props easily https://v3.vuejs.org/api/sfc-script-setup.html#defineprops-a... https://v3.vuejs.org/api/sfc-script-setup.html#typescript-on... (although vue class component is still the best API of all VDOMs, it's unfortunate its development has stalled)

Webstorm now properly supports template type checking too https://blog.jetbrains.com/webstorm/2021/11/webstorm-2021-3/...

Nobody should use the thing that the documentation tells you to use...

Vue 3 looks a lot better than Vue 2 but honestly it's just playing catch up to React. I don't know why you'd choose it.

You could generate an import map for Deno from the directory contents, and then your NodeJS-style imports would work there too. No file copying in that way.
This sounds just like the headaches I had about a year ago using ESM for isomorphic code, with node. The only thing that ever worked was, of course, Babel.
Yes had the same issue. Had to make deno run js files and import js produced by ts.
Whats the purpose of sharing types? Define entities on each and leave behavior as a domain consideration. Clients should be disposable and decoupled. If you want to render html on the server that's fine but svelte isn't the best choice for that.
> Whats the purpose of sharing types?

Server sends data as JSON to the client. Client parses JSON. Both now have the exact same data shape. Shared TS types let you use the same representation for the same data no matter where.

At the expense of coupling which is not a trivial concern. Regardless, the browser is the only platform capable of using those types (even so the types aren't shared but generated). You don't use those types in Swift or Java or Kotlin. You don't use those types in a python client library.
In my experience this coupling cannot be avoided. At a bare minimum the client has to know the communication protocol of the server. This means you typically need to define this protocol in both the server and client. If you could define it once and have both the client and server pull from the single source, it's a win.
The coupling is to the data interchange format. The server is not coupled to the client and the client is not coupled to the server.
Why does adding types result in any more coupling than what exists already?

If your client and server both treat field "temperature" as a string for protocol version 1, and then the client upgrades to protocol version 2 and temperature is now a float, the server has to be modified anyway, type system or no type system, because otherwise it'll break.

If anything, the type system helps to expose the fact that the client and server now disagree about the type of a field, which is helpful.

> Why does adding types result in any more coupling than what exists already?

Because the client and server now share a code base. The client requires the server to run.

> If your client and server both treat field "temperature" as a string for protocol version 1, and then the client upgrades to protocol version 2 and temperature is now a float, the server has to be modified anyway, type system or no type system, because otherwise it'll break.

If the data interchange format changes then yes both the client and server need to change. But if either the client or the server wants to coerce temperature to a different type they are free to do so. As long as the conform to the interchange format the internal representation of the data is irrelevant to third-parties.

In a world where types are shared, updates to the server necessitate updates to the client even if the client doesn't want to change (and vice-versa).

> If anything, the type system helps to expose the fact that the client and server now disagree about the type of a field, which is helpful.

The coupling should receive the credit not the type system. Regardless this undoubtedly true. Two isolated pieces of code don't know what the other is doing. You need to test and you need to write design docs.

Much of the point of TS is to make make this kind of refactoring easier, and it doesn't prevent coercing the data format later at all.
> Because the client and server now share a code base.

This isn't true. Types aren't "a code base", they're an interface - without which it is impossible for a client and server to talk with each other anyway.

> The client requires the server to run.

Not relevant. Nothing about a typed interface prevents the client from running without having a running server - it won't be very useful without something to exchange data with, but you can still run it, unless you've coded otherwise.

> But if either the client or the server wants to coerce temperature to a different type they are free to do so.

No, type coercions are bad engineering. They're brittle and only work in a very tiny number of situations - nothing approaching the general case.

> As long as the conform to the interchange format the internal representation of the data is irrelevant to third-parties. Type coercions at the interface level are a hack that is not acceptable for anything beyond hobbyist work.

An interchange format is a set of types. Types are not internal representation - they're a mathematical concept used in type-checking. Types happen to be used in the process of generating an internal representation of data, but that's not what they are. You're falsely equating the two, and your point is invalid without that equivalence.

> In a world where types are shared, updates to the server necessitate updates to the client even if the client doesn't want to change (and vice-versa).

This is false. If the server is changed, the client only needs to change if the interface between the two also changes. If the server makes a change to its internal data storage format, then that has nothing to do with the client, and so the interface stays the same, and so the shared type declarations/schema stay the same.

If the interface changes, then you already have to update both client and server, so the tiny overhead of maintaining an explicit type schema dwarfed by the amount of effort you spent changing the rest of the code anyway, and more than made up for by the compile-time detection of warnings when you change the interface code for the server, compile, and then get an error that the client no longer compiles.

> The coupling should receive the credit not the type system.

A type system will detect type errors at compile-time. A tightly-coupled system with type coercion and no types will detect errors at run-time, if at all. It's obviously better to detect errors at compile-time. Therefore, even if coupling is correlated with the usefulness of a type system, type systems are still obviously beneficial to include.

Regardless, it doesn't make any sense to talk about "credit" in this case. I think that you misunderstand the purpose (and utility) of type systems, but I'm not sure what to suggest to you so that you can understand.

> Regardless, the browser is the only platform capable of using those types

That and a Node/Deno backend. Which is what the OP is talking about.

What expense?
Sharing the types of API returns is very useful.
> Clients should be disposable and decoupled.

No, not really. Clients are loosely coupled, but they must comply with a contract.

And guess what: contracts define types, and REST focuses almost exclusively on types.

> Clients are loosely coupled

Under the architecture I describe, no, clients and servers do not couple to one another. The client and server couple to a data interchange format.

> contracts define types

Yes, the interchange format defines the types for the interchange. But a string can be represented as an enumeration or a float or an integer or a boolean. The type of the data is dependent on context.

> REST focuses almost exclusively on types

In a sense yes, but it also focuses on decoupling clients and servers. The types are really more of an implementation detail. The type of interchange can be XML, HTML, JSON, bytes, text, etc. The fields in a JSON document can all be strings for the purposes of interchange but may be parsed to integers and floats for the purposes of rendering the data to the user. The server may parse those fields differently.

Its not relevant to the client or the server how the other chooses to interpret the data. All that matters is that the interchange format is maintained.