惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
博客园_首页
Vercel News
Vercel News
WordPress大学
WordPress大学
美团技术团队
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Y
Y Combinator Blog
博客园 - 【当耐特】
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
腾讯CDC
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Recent Announcements
Recent Announcements
雷峰网
雷峰网
Last Week in AI
Last Week in AI
宝玉的分享
宝玉的分享
The Register - Security
The Register - Security
Jina AI
Jina AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
Help Net Security
Help Net Security
N
News and Events Feed by Topic
博客园 - Franky
P
Proofpoint News Feed
L
LINUX DO - 热门话题
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
D
Docker
Google DeepMind News
Google DeepMind News
有赞技术团队
有赞技术团队
IT之家
IT之家
Security Latest
Security Latest
L
LangChain Blog
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks

oida.dev | TypeScript, Rust

TypeScript's `erasableSyntaxOnly` Flag Unsafe for work Tokio: Macros Tokio: Channels Tokio: Getting Started Network Applications on the Tokio Stack Remake, Remodel, Reduce. The `never` type and error handling in TypeScript 5 Inconvenient Truths about TypeScript Refactoring in Rust: Introducing Traits Refactoring in Rust: Abstraction with the Newtype Pattern Announcing the TypeScript Cookbook TypeScript: Iterating over objects The road to universal JavaScript 10 years of oida.dev Rust: Tiny little traits How not to learn TypeScript Getting started with Rust Introducing Slides and Coverage TypeScript: The humble function overload TypeScript + React: Children types are broken TypeScript: In defense of any Rust: Enums to wrap multiple errors Dissecting Deno Error handling in Rust TypeScript: Unexpected intersections Upgrading Node.js dependencies after a yarn audit TypeScript: Array.includes on narrow types TypeScript + React: Typing Generic forwardRefs shared, util, core: Schroedinger's module names Learning Rust and Go TypeScript: Narrow types in catch clauses TypeScript: Low maintenance types Tidy TypeScript: Name your generics Tidy TypeScript: Avoid traditional OOP patterns Tidy TypeScript: Prefer type aliases over interfaces Tidy TypeScript: Prefer union types over enums My new book: TypeScript in 50 Lessons Go Preact! ❤️ this in JavaScript and TypeScript TypeScript and ECMAScript Modules TypeScript + React: Why I don't use React.FC TypeScript + React: Component patterns TypeScript: Augmenting global and lib.dom.d.ts Vite with Preact and TypeScript TypeScript: Union to intersection type 11ty: Generate Twitter cards automatically Are large node module dependencies an issue? TypeScript: Variadic Tuple Types Preview TypeScript: Improving Object.keys Remake, Remodel. Part 4. TypeScript + React: Typing custom hooks with tuple types TypeScript: Assertion signatures and Object.defineProperty TypeScript: Check for object properties and narrow down type Boolean in JavaScript and TypeScript void in JavaScript and TypeScript Symbols in JavaScript and TypeScript Why I use TypeScript TypeScript + React: Extending JSX Elements TypeScript: Validate mapped types and const context TypeScript: Match the exact object shape TypeScript: The constructor interface pattern Streaming your Meetup - Part 4: Directing and Streaming with OBS Streaming your Meetup - Part 3: Speaker audio Streaming your Meetup - Part 2: Speaker video Streaming your Meetup - Part 1: Basics and Projector TypeScript and React Guide: Added a new styles chapter TypeScript and React Guide: Added a new render props chapter TypeScript and React: Styles and CSS TypeScript and React TypeScript and React Guide: Added a new prop types chapter TypeScript without TypeScript -- JSDoc superpowers TypeScript: Mapped types for type maps JAMStack vs serverless web apps The Unsung Benefits of JAMStack Sites TypeScript: Ambient modules for Webpack loaders My most favourite talks in 2018 TypeScript and React Guide: Added a new context chapter TypeScript: Built-in generic types TypeScript: Type predicates JSX is syntactic sugar TypeScript and React Guide: Added a new hooks chapter Getting your CfP application right FAQ on our Angular Connect Talk: Automating UI development TypeScript and Substitutability Debugging Node.js apps in TypeScript with Visual Studio Code From Medium: Deconfusing Pre- and Post-processing From Medium: PostCSS misconceptions Saving and scraping a website with Puppeteer Cutting the mustard - 2018 edition Wordpress as CMS for your JAMStack sites My most favourite podcast episodes in 2017 My most favourite talks in 2017 My most favourite books in 2017 The Best Request Is No Request, Revisited Not so hidden figures - Organizing ScriptConf My podcast journey to ScriptCast Grid layout, grid layout everywhere! #scriptconf and #devone Object streams in Node.js
The TypeScript converging point
2022-01-31 · via oida.dev | TypeScript, Rust

Usually, when doing TypeScript talks, I just open up a code editor and hack away some cool types that help in a certain scenario. This time, I was asked to do the same thing but within a 20-minute time limit. This has been super tough, so I scripted the entire thing and resorted to slides that have certain progress. Fewer chances for me to screw up! This allows me to give you not only the slides but also a write-up of this talk. I’ll give myself a bit of freedom and flesh it out where appropriate. Enjoy!

Transcript #

So recently I came upon a nice little library called commander. It helps you create Node.js CLIs, parsing your arguments and providing you with an object with all the flags you set. The API is glorious, as you’d expect by its author.

The API looks something like this:

const program = new Commander();

const opts = program
.option("--episode <number>", "Download episode No. <number>")
.option("--keep", "Keeps temporary files")
.option("--ratio [ratio]", "Either 16:9, or a custom ratio")
.opts();

if (!opts.keep) {
// Remove all files
}

What I like is that you write your application like you would write your man page or your help dialog. You write it as you would read it. This is fantastic and one of the nice things in JavaScript that I miss from many other programming languages: The flexibility you get with strings.

In this example, we deal with three possibilities:

  • Mandatory arguments, where we are required to pass a string value
  • Flags, either true or false
  • Optional arguments, either not set (false), set (true), or set with a string value.

Also, there’s a nice fluent interface. A builder pattern. This is the stuff that makes APIs just nice.

One thing that bugs me though is that I always need to refer to the options I set to know which flags are available and what they mean. This is where clumsy me constantly stumbles upon errors and typos. You know what happens if I call my option --keeps but ask for not keep? Yes, since keep would be undefined, we always execute the part where we delete our files.

Or what if I change ratio to a mandatory argument instead of an optional one? Suddenly all checks where I assume ratio is a boolean would be wrong.

There’s lots of potential for types here. So I tried to design some!

Basic types #

The first thing I do when designing types is get the basic types right. Here, I design a Command type that features two methods.

type Command = {
option(command: string, description?: string): Command
opts(): Record<string, any>
}
  • option takes a command of type string and an optional description. It returns Command again. This is how we describe the fluent interface.
  • opts gives us the result. Right now it’s a Record with string keys. So it’s any object. TypeScript will just let you pass once you access props with key.

Frankly, those types are not that helpful. But we’re getting there.

Next, we also create the constructor function that creates a Command object.

type Commander = {
create(): Command
}

Nothing out of the ordinary. Let’s declare a class (so we don’t need to bother with the implementation) and see what we can do already.

declare const Commander: Commander;

const program = Commander.create();

Nothing much. Plus the API is not what we expect. We don’t want to call Commander.create(). We want to instantiate a new class:

const program = new Commander();

Achieving this is remarkably easy. Check this out.

type Commander = {
- create(): Command
+ new(): Command
}

One line. We only need to change one single line. The new() function tells TypeScript that this is an actual constructor function, which means we can call new Commander() to instantiate a new class. This works because every class in JavaScript gives you two interfaces: one for the static parts and the constructor function, and one for the elements of an instance. There’s a similarity to how prototypes and constructor functions worked before there were classes. You can read up on constructor interfaces in this article.

So now that this works, we want to create better types for the instance we create.

Adding generics #

The next step in this progress is adding generics. We can use generics to get to the actual value types or literal types of the strings we add as parameters. We replace the first argument command with a generic variable U that extends string.

type Command = {
option<U extends string>(command: U, description?: string): Command
opts(): Record<string, any>
}

With that, we are still able to pass strings, but something interesting happens. Every time we put in a literal string, we can narrow down the type to the exact literal type. Look at this identity function for example:

function identity<T>(t: T):T { return t }

const x = identity<string>("Hello World")
const y = identity("Hello World")

The only purpose of this is to bind T to a type and return the same value. If we instantiate the type variable with a type like in the first example, the return value’s type – the type of x – is also string. In the second example, we let TypeScript infer by usage. The second example’s return type – the type of y – is the literal string "Hello World". So every value is also a type. And we can get to this type by using generic type variables. This is I guess the most important lesson on generic type variables. If you take one thing home, it’s this.

Back to our example. So with every call of .option we bind the literal string to U. We now need to collect this literal string and pass it along with every usage. To do so, we add another generic type variable T as an accumulator.

type Command<T> = {
option<U extends string>(command: U, description?: string): Command<T>
opts(): Record<string, any>
}

And instantiate this generic type variable with the empty object.

type Commander = {
new(): Command<{}>
}

Now, with every call of option, we take U and add it to the empty object. We use a Record for now.

type Command<T> = {
option<U extends string>(command: U, description?: string)
: Command<T & Record<U, any>>
opts(): T
}

We also return T when calling opts(). Remember, T stores our accumulated options. The effect? Check it out:

const opts = program
.option("episode", "Download episode No. <number>")
.option("keep", "Keeps temporary files")
.option("ratio", "Either 16:9, or a custom ratio")
.opts();

When calling opts(), we get back an object of the following type:

const opts:
Record<"episode", any> &
Record<"keep", any> &
Record<"ratio", any>

This means we can access opts with the keys episode, keep, and ratio. Cool, that’s pretty close to the real deal!

Going further #

But we are not there yet. The API of commander is much more advanced. We can write man pages! We can use double-dashes to tell our intent.

const opts = program
.option("--episode", "Download episode No. <number>")
.option("--keep", "Keeps temporary files")
.option("--ratio", "Either 16:9, or a custom ratio")
.opts();

With the current types, the type of opts looks like this:

const opts:
Record<"--episode", any> &
Record<"--keep", any> &
Record<"--ratio", any>

This means we would access our options like this: opts["--episode"]. Not cool. Let’s improve!

Instead of using a Record to collect keys, we replace it with a new type called ParseCommand<T>.

type Command<T> = {
option<U extends string>(command: U, description?: string)
: Command<T & ParseCommand<U>>
opts(): T
}

ParseCommand is a conditional type that looks like this.

type ParseCommand<T extends string> =
T extends `--${string}` ? { [k in T]: boolean } : never;

We check for T, which extends string, if the T we pass extends a string that starts with "--". We say “are you a subset of all strings that start with a double dash”? If this condition is true, we return an object where we add T to our keys. Since we only pass one literal string each time we call .option(), we effectively check if this string starts with two dashes. In all other cases we return never. never is great because it tells us that we are in a situation which can never happen. An intersection with never makes the entire type never. We can’t access any key at all from opts. This is great! It shows us that we added something to .option which might cause an error. Our software wouldn’t work and TypeScript tells us so by adding red squiggly lines everywhere we want to use the result!

One conditional type more, still no progress. We are not only interested if our string starts with two dashes, we also are interested in the part that comes after those dashes. We can instruct TypeScript to fetch that literal type out of this condition, to infer the literal type, and use this one instead:

type ParseCommand<T extends string> =
T extends `--${infer R}` ? { [k in R]: boolean } : never;

And with this single line change, we completed our type. Just two lines of code, and we can write something like this:

const opts = program
.option("--episode", "Download episode No. <number>")
.option("--keep", "Keeps temporary files")
.option("--ratio", "Either 16:9, or a custom ratio")
.opts();

And get a type that looks like this. Simply beautiful.

const opts: {
episode: boolean;
} & {
keep: boolean;
} & {
ratio: boolean;
}

But we not only want to check for flags, but we also have optional or mandatory arguments. We can extend our string template literal type that strips away the double dashes with more use cases:

type ParseCommand<T extends string> =
T extends `--${infer R} <${string}>` ?
{ [k in R]: string } :
T extends `--${infer R} [${string}]` ?
{ [k in R]: boolean | string } :
T extends `--${infer R}` ?
{ [k in R]: boolean } :
never;

Nested conditional types that check on string template literal types. Wow! What a mouthful. The result: We write something like this:

const opts = program
.option("--episode <number>", "Download episode No. <number>")
.option("--keep", "Keeps temporary files")
.option("--ratio [ratio]", "Either 16:9, or a custom ratio")
.opts();

And get this type for opts.

const opts: {
episode: string;
} & {
keep: boolean;
} & {
ratio: string | boolean;
}

Stunning!

More extravaganza! With a union type of a nested string template literal type and the empty string inside a string template literal type in a nested conditional type – breathe, breathe – we can even check for optional shortcuts.

type ParseCommand<T extends string> =
T extends `${`-${string}, ` | ""}--${infer R} <${string}>` ?
{ [k in R]: string } :
T extends `${`-${string}, ` | ""}--${infer R} [${string}]` ?
{ [k in R]: boolean | string } :
T extends `${`-${string}, ` | ""}--${infer R}` ?
{ [k in R]: boolean } :
never;

So when we write something like this:

const opts = program
.option("-e, --episode <number>", "Download episode No. <number>")
.option("--keep", "Keeps temporary files")
.option("--ratio [ratio]", "Either 16:9, or a custom ratio")
.opts();

Hah… no, check it out yourself. Head over to the playground and give it a go.

The converging point #

What we got is type safety for programs that live by using a flexible, string-based API. We transformed string types to strong types. All with just a couple lines of code and some of the more advanced features of TypeScript.

With all that power, I wonder: Have we reached a converging point? Can we express every JavaScript program through TypeScript types?

The answer is: No. TypeScript is powerful, no doubt. But one thing that I’ve hidden from you is that those types only work so well because I use them in a specific way. When I stick to the builder pattern, everything is hunky-dory. If I use my program differently, I end up in a state I can’t express through types. Not even with assertion signatures.

program
.option("-e, --episode <number>", "Download episode No. <number>")
.option("--keep", "Keeps temporary files");

program
.option("--ratio [ratio]", "Either 16:9, or a custom ratio")

const opts = program.opts(); // The empty object :-(

Well, at least not yet. TypeScript’s goal is to make as much of JavaScript expressable through its type system. And as you’ve seen, we got pretty far already. If use-cases like this become even more popular, TypeScript will inevitably add a feature to support this. And it’s ok for TypeScript to catch up to JavaScript. It always did. And JavaScript’s flexibility lead us to wonderful APIs that help us create good programs, that continuously led to a lower barrier for newcomers, and that made libraries like jQuery, express.js, or Gulp so popular. And I like that even in 2022 I can get excited by a lovely, little library like commander. And I’m excited to see what TypeScript will have in stores for situations like this.

Resources

Public presentations