Hacker News new | past | comments | ask | show | jobs | submit login
An introduction to type programming in TypeScript (zhenghao.io)
148 points by bpierre on Feb 2, 2022 | hide | past | favorite | 65 comments



I might just be not as intelligent as the average TypeScript developer, but I find the complexity with the type system off-putting. There's 5 different ways to type something, and learning a new type language isn't what I wanted when I just needed my JavaScript to be typed.

It's too bad Flow has lost the typed-JS battle, I find the syntax to be a lot easier to use.


Someone created a very nice curated list of advanced Typescript videos. [0] After watching and working through them, I have not been stumped with more complex typing problems.

[0] https://www.youtube.com/playlist?list=PLw5h0DiJ-9PBIgIyd2ZA1...


The key is to ignore 90% of TypeScript's features in the average application-level codebase. I avoid even using named generic types, much less conditional types etc.

Unfortunately, having all that power available is necessary because it has to be able to reasonably describe a highly-dynamic language. The more dynamic your JS is to begin with, the crazier your types will have to be to make sense of it. But most of that craziness can be avoided if you write straightforward logic with straightforward data structures to begin with.


To expand on this, as someone with a deep sense of Typescript's features I start to flag them if I see them in PRs to applications. A lot of Typescript's features are for other people's code or for building blocks to smarter features (conditional types are interesting sure, but they sing in things like ReturnType<T>; you don't need to know the conditional type underpinnings to use the higher level ReturnType<T> feature).

I've had to break out some wild types to describe JS modules I've needed. If I need such things inside a TS application I control, I often see that as a failure to write clean/concise code in the domain language of the application itself rather than some "ideal" abstraction that may not matter (and I expect will eventually crumble into unreadable tech debt).


> and I expect will eventually crumble into unreadable tech debt

Or crumble when tsc tries to follow them ;)

I’ve sometimes found, perhaps for the better, that when I get too creative with types the typescript compiler will at some point just not be able to follow what I’m trying to tell it any more. Even though my types are correct, theoretically, it will just sort of tip over. That usually serves as a good sign that I shouldn’t be trying to do what I’m doing at all.


> and learning a new type language isn't what I wanted when I just needed my JavaScript to be typed.

Well, good news, you can use Typescript's compiler as a Js static code analyzer without writing a single line of Typescript!


Strongly disagree.

It is OK if you prefer no solution to some complex typing needs that are solved with complex syntax in Typescript.

Most of Typescript is optional.

It's not trivial to think of simpler alternatives to much of the problems Typescript is tackling.

(And if you have any simplifying syntax suggestions that don't break typing it will be interesting to read)


> but I find the complexity with the type system off-putting

Yeah, is it. But "complexity" is fine:

    Complex is better than complicated.
         - [Zend of python](https://www.python.org/dev/peps/pep-0020/)
What is a big trouble is when the complexity is NOT intentionally designed (ie: in languages like oCalm, Rust, Coq...) but instead the type system is a lie that bring "complications":

    //A bad language, like JS, with both complexity AND complications:
    [] - {}; // NaN
    
Humans can deal with complexity if is part of a intentional design (example: APL), but what we dread is when is accidental...


    [] - {}
is a fatal typescript error[0]. A well configured linter can also catch out similar nonsense, but [] - 2 does get past the compiler.

The TS Compiler can atleast stop you from doing nonsense like applying operators to types where they don't make sense. Of course, it'd be better if JS just threw an error or something, but atleast TS can stop you.

[0]: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.ts(2363)


I’m curious about the example of unintentional complexity and type system lies in Rust?

Or did you mean to say that oCaml, Rust, and Coq have intentional design complexity?


> have intentional design complexity?

yes, more like this (in special Rust, that deal with many challenges like system programming, safety, etc).


Typescript can type all your existing JS, but you're right it's crazy complex. TS only becomes manageable when you stop treating the compiler as an add-on and treat it as a guide.


You almost never need to reach for advanced typing unless you're writing a generic library.


Check out Hegel[0], it uses Flow syntax, it's compatible w/ .d.ts type definitions and has a smarter type inference model than both TS and Flow IMHO[1].

[0] https://hegel.js.org/

[1] https://www.reddit.com/r/JSdev/comments/nl4ccq/is_flow_movin...


I absolutely agree. But I assume that at least some of the complexity also comes from what JS can do so typing some things might inherently be complicated. But trying to type an arrow style react component with generics in it‘s props has been incredibly tough an I am absolutely sure I didn‘t do it correctly (tsx also getting in on the fun is another complexity making just figuring out how to even write certain things challenging).


Ha. It's not you. It's a shit show everything is a afterthought. It's incredibly inconsistent and clunky all over. I just don't try to get carried away with typing stuff too smartly.

Most of this is just the way the language evolved, along JavaScript. I wish they'd abandon the "JavaScript superset idea" and cleaned up the language.

Make the common case simpler and more straightforward, drop everything exotic, it's not practical nor strictly necessary.


This is fantastic writing:

  After writing TypeScript for a while, it occurred to me that the TypeScript language actually consists of two sub-languages - one is JavaScript, and the other is the type language.

  For the JavaScript language, the world is made of JavaScript values; for the type language, the world is made of types.


This is called a "biformity". There was a great post about this the other day:

https://hirrolot.github.io/posts/why-static-languages-suffer...



You might be interested to learn that in languages like Coq with dependent types, the difference between type-level and value-level languages (almost) disappears! Check out the Software Foundations book [1].

[1] https://softwarefoundations.cis.upenn.edu/


The article seems to be mixing up type synonyms with type variables. Type variables have an actual well defined meaning. They act like variables that can be instantiated during inference/checking, analogue to how variables in the expression language get instantiated at runtime.


> Type variables have an actual well defined meaning.

In TypeScript or JavaScript? Can you say more?


Type variables are sometimes called generics and a part of the type language, so JavaScript doesn’t have them. Typescript, but also Rust, Haskell, Java, the ML-family have them.

Type variables abstract over types and are analogous to function parameters at the values level. Think the ‘T’ in ‘List<T>’.


Thanks


Cool introduction, now I'm one step closer to typing the technical interview https://aphyr.com/posts/342-typing-the-technical-interview


I think people focus on the "turing complete" part of type checking a bit too much - what's more compelling (and a real design decision) is that when designing a type system you need to choose if the type checking algorithm is decidable. That's not usually an accident, since when implementing the system you need to write the type checker, and you'll see quite plainly if it has an infinite loop case.


A turing complete type system is undecidable.


I think it's tolerable if the recursion limit is low, preferably single digit.


My point is that's not as interesting or practical as a type system for which you can write an expression that can't be checked. They may be roughly equivalent, but only one is esoteric.


I love writing code with typescript, but some things just make it harder than necessary, for example using environment variables.

If I want to use process.env.NODE_ENV I first have to create a new environment.d.ts, declare the interfaxe and export an empty object just so the typescript compiler is satisfied. It sometimes just adds too much boilerplate.

https://stackoverflow.com/a/53981706


Don't do this! What TS is trying to tell you is that this variable may not have been set in the environment, and thus it's value inside of the program will evaluate to `undefined`.

What you are doing when you create the environment.d.ts file is forcing the transpiler to assume the variable will have been set, which is not really true. The program may still end up being executed in an environment in which the variable was not set.

Instead, what I like to do is create a config file in your project where you export an object containing all configuration values for your project. There, you can set default values for variables coming from the environment. In this way, you guarantee a value will be present for the execution of your program:

  export const config = {
    dbConnectionString: process.env.DB_CONNECTION || 'localhost:27017',
    nodeEnv: process.env.NODE_ENV || 'development',
  }

Remember that the NodeJS runtime guarantees the setting of no environment variables. The NODE_ENV variable is simply a convention that was started by express.


Stuff like `dbConnectionString: process.env.DB_CONNECTION || 'localhost:27017'` is a good way to boot your app with the wrong environment variables and potentially cause security issues. I'd advise to make sure your app doesn't start or deploy if expected env vars aren't set.


If you do have some environment variable for which no safe fallback value may exist, you can implement that logic in the config file also. I was just giving a cursory example ;)


Don’t forget the nullish coalescing operator, ??, which is probably nicer than using || here.

https://www.typescriptlang.org/docs/handbook/release-notes/t...


You're right, that is a much better choice for this situation!


> Remember that the NodeJS runtime guarantees the setting of no environment variables. The NODE_ENV variable is simply a convention that was started by express.

You're right that the runtime itself doesn't guarantee any env vars. I'm not sure who used it first, but npm also sets NODE_ENV (and some other env vars I think) in its different lifecycles, depending on if you provide the --production/--no-production flag when running it. But not sure if that came after or before express started using it.


Typescript’s type narrowing is useful for this exact situation:

  // v is any or unknown
  if (typeof v !== "string") {
    throw new Error("oops")
  }
  // v is now string
You can’t get runtime type correctness by deceiving typescript into it.


Rather than declaring it yourself every time, there is an @types/node you can install with all of the Node-specific types including process.env:

https://www.npmjs.com/package/@types/node

(ETA: It's mentioned in the StackOverflow and they still add an extra definition on top of it, but the env in the types today is a Record and you don't have to do it, you just won't get auto-completion on the names.)


This is why I think non-null assertion should be accepted by default. If you forget to set the DB connection string, my backend is essentially useless! I can’t persist anything. I do process.env.DB_CONN! - at runtime I’ll get an error that the string is undefined, but that’s actually what I want - I want my app to throw an error so I can quickly find the error, slap my forehead and put the variable in the .env file.


Here’s an implementation of lambda calculus (which is TC) at type level in TypeScript: https://github.com/EvolveYourMind/ts-lambda-calc

And here a type-level RegExp matcher: https://github.com/EvolveYourMind/ts-regexp


Another example, SQL-database-in-the-typescript-typesystem here https://github.com/codemix/ts-sql

This would have been much easier if I'd had ts-regexp at the time :)


I was actually inspired by your ts-sql project! Amazing work


I really like JS/TS Combo. My only critic is the Enums - particularly the String enums. It is really an absolute mess.


“Bare” enums are not better either. No introspection, bad debug-ability (you can’t print them in a human-readable way). Typescript enums are a good tradeoff between just string|string|… types and pure integers (or whatever type you assign to them). The former’s literals are indistinguishable from string literals at readsourcestime. The latter creates issues at debugtime.


Union types are the way to go in 99% of cases you need enums.


Yes, and generating those unions from a const array gives you even further flexibility with runtime access: `const myStrArray = ["a", "b", "c"] as const; type MyStrType = typeof myStrArray[number];`


It's eye-opening to know the Turing completeness of TypeScript' type programming. It is fun and insightful to know this fact. The examples are simple and to the point.


This really shows that there is a lot of potential for a native tsc alternative (like the one that was on the front page recently) to shine in terms of performance


I think it shows a lot of potential to turn TypeScript to what C++ has become.


Unlikely, types intentionally cannot affect the emitted JavaScript. You can always get raw JavaScript from TypeScript code by blindly erasing the type annotations (and transforming a few special syntaxes).


I just remembered this blog post, maybe that's the one that was on the front page recently ?

https://kdy1.dev/posts/2022/1/tsc-go

Ok yup, definitely that one, https://news.ycombinator.com/item?id=30074414


My main issue with TypeScript is the errors or warning in vscode are always awful

edit: I am used to working with Java in Eclipse where the type warnings make much more sense


The author started the topic really well, be defining a separation between Javascript/Typescript.

I find the examples that followed to be a demonstration without purpose.

After reading them I don't know when to use them, he mentions "type gymnastics" at the end and I find that most of the examples are demonstrations of type gymnastics.

I work on a large Typescript codebase and rarely I need to do abstract typing in order to get something done, Typescript can be used in a very direct and objetive manner and it's usually the route I prefer.


Having a Turing complete type system is not what you normally want to have, that (it means to have undecidable type checking) makes tooling (and the compiler) way more complex and difficult.

GitHub issue about the Turing completeness:

https://github.com/microsoft/TypeScript/issues/14833

Proof of Turing completeness:

https://gist.github.com/hediet/63f4844acf5ac330804801084f87a...


Thanks but I'll take the ability to write arbitrarily complex type-checks over anything else. As a C++ dev who goes out of his way to express things of the type system this has only ever been a positive for me, the loss in compile-time is more than offset by the ability to eliminate whole classes of bugs in one go.


If you want arbitrarily complex type-checking, why not just check the property you want at runtime instead using an esoteric typelevel programming language? The whole point of having types in the first place is provable decidability, not universal computation -- that's what the value-level is designed to do.


Which widely used statically typed languages do not have a Turing complete type system? C, probably Go?


Depends on your definition of 'widely used' ;).

Using Tiobe's top 20: I'd guess Swift, Delphi, Fortran and Go

C++, Java, Rust, Haskell, Typescript have a Turing complete type system. C#: don't know, could be.


Of the list above, I'm most surprised that Swift doesn't have a Turing complete type system.


I wanted to answer with "because Swift's type checking is decidable", but that has changed (with SE-0142 and SE-0157) - allowing recursion. So, it is 'at least' undecidable now.

https://forums.swift.org/t/swift-type-checking-is-undecidabl...


Yes, I did see that bit. Incidentally google provides terrible results for 'swift type system turing complete'.


Here's an implementation of a prime finding algorithm in pure (only type-level) Typescript.

    type IsPrime<
      MaybePrime extends number,
      NumAsTuple extends unknown[] = [],
      RemainderAsTuple extends unknown[] = NumAsTuple,
    > =
      MaybePrime extends NumAsTuple["length"]
        ? NumAsTuple["length"] extends RemainderAsTuple["length"]
          ? NumAsTuple["length"] extends (1 | 2 | 3 | 4 | 5 | 6)
            ? NumAsTuple["length"] extends (1 | 2 | 3 | 5)
              ? true
              : false
            : NumAsTuple extends [infer _, infer _, infer _, infer _, infer _, infer _, ...infer Rest]
              ? IsPrime<MaybePrime, NumAsTuple, Rest>
              : false
          : RemainderAsTuple["length"] extends (1 | 2 | 3 | 4 | 5 | 6)
            ? RemainderAsTuple["length"] extends (1 | 5)
              ? true
              : false
            : RemainderAsTuple extends [infer _, infer _, infer _, infer _, infer _, infer _, ...infer Rest]
              ? IsPrime<MaybePrime, NumAsTuple, Rest>
              : false
        : IsPrime<MaybePrime, [...NumAsTuple, 1]>;


thank you! here[1] it is in action in the playground.

[1]: https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgzgBQE4...


Cool. How large prime can it detect before it hits the recursion limit?


857




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: