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

推荐订阅源

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 The TypeScript converging point 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 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
TypeScript: Mapped types for type maps
2019-07-15 · via oida.dev | TypeScript, Rust

Factory functions are a popular tool in JavaScript to create a diversity of objects with a single call. There’s a particular factory function that you might have used at some point:

document.createElement('video') // creates an HTMLVideoElement

document.createElement takes any string and creates HTMLElement instances. HTMLElement is one of the most derived objects in the DOM. Every available tag creates it’s on derivate. For example document.createElement('video') creates an instance of HTMLVideoElement.

But how do we type a factory function like that? One that has a couple of dozen different return types? Let’s try.

NOTE: TypeScript with the dom library activated in tsconfig.json knows of all HTMLElement derivates.

With conditional types #

The original typings for document.createElement take a string as parameter, and returns an object of type HTMLElement:

declare function createElement(tag: string, options?: any): HTMLElement

This is true, but not specific enough. We can be a lot more specific, since we know which tags implement corresponding HTMLElement derivates.

The first thing that came into my mind were conditional types. They were made for use cases like that! We know that tag is a subset of string, and more important: we know exactly which subset! The collection of tags available in your browser.

A possible generic type for the createElement factory function could look like this:

type CreatedElement<T extends string> =    /** 1 **/
T extends 'a' ? HTMLAnchorElement : /** 2 **/
T extends 'div' ? HTMLDivElement :
T extends 'video' ? HTMLVideoElement :
HTMLElement; /** 3 **/

Let’s go over this definition in detail:

  1. We start with a generic type CreatedElement<T>. The generic placeholder T has to be a subset of string
  2. We then test for a specific unit type from the string subset. For example, the string "a" is of type string, but also of type "a". You can think of string as the universe of all possible string unit types. If our generic placeholder T extends this particular subset of "a", we know that the result has to be of type HTMLAnchorElement. The else branch starts a cascade through all known HTML tags. In our example, we know of div and video.
  3. At the end of this cascade, when we went through all known HTML tags, we return the generic HTMLElement as a fallback. This is totally in tune with the way createElement works. When you create an element with a tag the browser doesn’t know, you get at least an HTMLElement.

So far, so good. This even looks like a map from string to HTMLElement derivate. Now the only thing we have to do is to extend this list with all available tags and return the respective element instance. We can even use things like union types to help with types that implement more than one tag:

type CreatedElement<T extends string> = 
T extends 'a' ? HTMLAnchorElement :
T extends 'div' ? HTMLDivElement :
T extends 'video' ? HTMLVideoElement :
T extends 'thead' | 'tfoot' | 'tbody' ? HTMLTableSectionElement : /** 1 **/
HTMLElement;
  1. All three of thead, tbody and tfoot implement HTMLTableSectionElement. We can use a union type of all three unit types to identify HTMLTableSectionElement

The solution is good and robust, but has one catch. A rather big one. The amount of comparisions is finite. Even though this looks like a map of types, in reality it’s a nested comparision chain:

type CreatedElement<T extends string> = 
T extends 'a' ? HTMLAnchorElement :
T extends 'div' ? HTMLDivElement :
T extends 'video' ? HTMLVideoElement :
T extends 'thead' | 'tfoot' | 'tbody' ? HTMLTableSectionElement :
HTMLElement;

So it’s nested. This also means that with every further comparision, there has to be reference to the original comparison. Internally, this can be best done via a recursion. And recursions take up memory.

This is why TypeScript gives you a hard limit of 50 nested comparisons to make sure memory and performance goals are met. If you extend your list beyond 50 comparisions, you get the error “Type instantiation is excessively deep and possibly infinite”. Check out the issue #28663 on Github.

So that doesn’t solve our problem in the long run. So what shall we do?

Mapped types #

To find a proper solution for that problem, let’s think for one moment how you would’ve implemented the document.createElement factory function yourself in JavaScript. I would have used an object, where each key corresponds to the correct implementation. And I would’ve indexed the object dynamically with an index access. Something like that:

NOTE: This does not work. This is just for demonstration purposes

const elementMap = {
a: HTMLAnchorElement,
div: HTMLDivElement,
video: HTMLVideoElement
}

function createElement(tag) {
return tag in elementMap ? new elementMap[tag]()
: new HTMLElement()
}

This obviously doesn’t work, that’s what the document.createElement factory function is for. But it illustrates the way of accessing via the index access operator rather nice. Since every key in an object can be accessed using a string, we select the right constructor out of this list, and create a new instance. If we don’t have a constructor, let’s create a generic HTMLElement.

In TypeScript, we can create types that work in a similar manner. First, let’s create the AllElements type which is a map of all tags to their corresponding HTMLElement derivate:

type AllElements = {
'a': HTMLAnchorElement,
'div': HTMLDivElement,
'video': HTMLVideoElement,
... //extend as you need
}

This is what I like to call a type map. We could use this type to create an object of type AllElements, but in reality we most likely won’t need that. We only use this type as an helper type for CreatedElement:

type CreatedElement<T extends string> = 
T extends keyof AllElements ? AllElements[T] : /** 1 **/
HTMLElement; /** 2 **/
  1. The type signatur is the same as in the previous example. The generic placeholder T extends from string. But now we check if T is somewhere in the keys of AllElements. If so, we index the type that is stored with this particular key T. That’s how we get the correct derivate!
  2. In all other cases, we have “just” an HTMLElement.

Do you see how similar this type definition is to the JavaScript example above? Of course the way I wrote JavaScript earlier is just one way to express myself, and I used it deliberately to show the similarities with conditional types. But it shows how close TypeScript tries to be to JavaScript in terms of syntax and semantics.

The cool thing is: We are just moving in type space. No source created, just information to make your code a lot safer. Like that:

declare function createElement<T extends string>(tag: T, options?: any): CreatedElement<T>

We can use the function like that:

createElement('video') // It's an HTMLVideoElement
createElement('video').src = '' // `src` exists
createElement('a').href = '' // It's an HTMLAnchorElement with an `href`

We can even write our own factory functions, that can do a little more that just creating elements:

function elementFactory<T extends string>(tag: T, 
defaultProps: Partial<CreatedElement<T>>) : CreatedElement<T> {

const element = createElement(tag);
return Object.assign(element, defaultProps)
}

This factory takes a couple of default properties that need to be available in the generated output element. So things like:

elementFactory('video', { src: '' });

Can even be autocompleted. And TypeScript warns you if you want to specify a property that does not exist:

elementFactory('video', { source: '' }) // 💥 Error: this property does not exist

Pretty sweet, huh?

Bottom line #

Type maps are a good tool for factory functions which produce a ton of different results. And most likely for a lot, lot more. If you want to expand on the example shown above, take this gist. And look at this playground to play around.

Related Articles