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

推荐订阅源

Hacker News - Newest:
Hacker News - Newest: "LLM"
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs
H
Heimdal Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
N
News and Events Feed by Topic
H
Hacker News: Front Page
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
SecWiki News
SecWiki News
N
News | PayPal Newsroom
T
Tor Project blog
W
WeLiveSecurity
A
Arctic Wolf
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
月光博客
月光博客
AWS News Blog
AWS News Blog
D
Docker
C
CERT Recently Published Vulnerability Notes
MyScale Blog
MyScale Blog
Google Online Security Blog
Google Online Security Blog
大猫的无限游戏
大猫的无限游戏
T
The Blog of Author Tim Ferriss
I
InfoQ
人人都是产品经理
人人都是产品经理
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
Hacker News: Ask HN
Hacker News: Ask HN
Blog — PlanetScale
Blog — PlanetScale
博客园 - 【当耐特】
Engineering at Meta
Engineering at Meta
Stack Overflow Blog
Stack Overflow Blog
Recorded Future
Recorded Future
罗磊的独立博客
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
The Exploit Database - CXSecurity.com
D
DataBreaches.Net
S
Security Affairs
WordPress大学
WordPress大学
T
Threatpost
Microsoft Security Blog
Microsoft Security Blog
V
Vulnerabilities – Threatpost
The Hacker News
The Hacker News
S
SegmentFault 最新的问题
B
Blog RSS Feed
Project Zero
Project Zero
P
Proofpoint News Feed

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 + 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
TypeScript: The humble function overload
2021-11-30 · via oida.dev | TypeScript, Rust

With the most recent type system features like conditional types or variadic tuple types, one technique to describe a function’s interface has faded into the background: Function overloads. And there’s a good reason for that. Both features have been implemented to deal with the shortcomings of regular function overloads.

See this concatenation example directly from the TypeScript 4.0 release notes. This is an array concat function:

function concat(arr1, arr2) {
return [...arr1, ...arr2];
}

To correctly type a function like this so it takes all possible edge cases into account, we would end up in a sea of overloads:

// 7 overloads for an empty second array
function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
// 7 more for arr2 having one element
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];
// and so on, and so forth

And this only takes into account arrays that have up to six elements. Variadic tuple types help greatly with situations like this:

type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}

You can easily see how boils down the function signature to its point while being flexible enough for all possible arrays to come. The return value also maps to the return type. No extra assertions, TypeScript can make sure that you are returning the correct value.

It’s a similar situation with conditional types. This example comes directly from my book. Think of software that retrieves orders based on customer, article, or order ID. You might want to create something like this:

function fetchOrder(customer: Customer): Order[]
function fetchOrder(product: Product): Order[]
function fetchOrder(orderId: number): Order
// the implementation
function fetchOrder(param: any): Order | Order[] {
//...
}

But this is just half the truth. What if you end up with ambiguous types where you don’t know exactly if you get only a Customer, or only a Product. You need to take care of all possible combinations:

function fetchOrder(customer: Customer): Order[]
function fetchOrder(product: Product): Order[]
function fetchOrder(orderId: number): Order
function fetchOrder(orderId: Customer | Product): Order[]
function fetchOrder(orderId: Customer | number): Order | Order[]
function fetchOrder(orderId: number | Product): Order | Order[]
// the implementation
function fetchOrder(param: any): Order | Order[] {
//...
}

Add more possibilities, you end up with more combinations. Here, conditional types can reduce your function signature tremendously.

type FetchParams = number | Customer | Product;

type FetchReturn<T> = T extends Customer ? Order[] :
T extends Product ? Order[] :
T extends number ? Order: never

function fetchOrder<T extends FetchParams>(params: T): FetchReturn<T> {
//...
}

Since conditional types distribute a union, FetchReturn returns a union of return types.

So, there is good reason to use those techniques instead of drowning in too many function overloads. This bears the question: Do we still need function overloads?

TL;DR: Yes, we need function overloads.

Here are a few examples.

Different function shapes #

One scenario where function overloads still are very handy is if you have different argument lists for your function variants. This means that not only the arguments (parameters) themselves can have some variety (this is where conditionals and variadic tuples are fantastic), but also the number and position of arguments.

Imagine a search function that has two different ways of being called:

  1. Call it with the search query. It returns a Promise you can await.
  2. Call it with the search query and a callback. In this scenario, the function does not return anything.

This can be done with conditional types, but is very unwieldy:


// => (1)
type SearchArguments =
// Argument list one: a query and a callback
[query: string, callback: (results: unknown[]) => void] |
// Argument list two:: just a query
[query: string];

// A conditional type picking either void or a Promise depending
// on the input => (2)
type ReturnSearch<T> = T extends [query: string] ? Promise<Array<unknown>> : void;

// the actual function => (3)
declare function search<T extends SearchArguments>(...args: T): ReturnSearch<T>;

// z is void
const z = search("omikron", (res) => {

})

// y is Promise<unknown>
const y = search("omikron")

Here’s what we did:

  1. We defined our argument list using tuple types. Since TypeScript 4.0, we can name tuple fields just like we would do it with objects. We create a union because we have two different variants of our function signature
  2. The ReturnSearch type selects the return type based on the argument list variant. If it’s just a string, return a Promise, if it has a callback, return void.
  3. We add our types by constraining a generic variable to SearchArguments, so that we can correctly select the return type

That is a lot! And it features a ton of complex features that we love to see in TypeScript’s feature list: Conditional types, generics, generic constraints, tuple types, union types! We get some nice auto-complete, but it’s nowhere the clarity of a simple function overload:


function search(query: string): Promise<unknown[]>
function search(query: string, callback: (result: unknown[]) => void): void
// This is the implementation, it only concerns you
function search(query: string, callback?: (result: unknown[]) => void): void | Promise<unknown> {
// Implmeent
}

We only use a union type for the implementation part. The rest is very explicit and clear. We know our arguments, we know what to expect in return. No ceremony, just simple types. The best part of function overloads is that the actual implementation does not pollute the type space. You can go for a round of anys and just don’t care.

Exact arguments #

Another situation where function overloads can make a lot of things easier is when you are in need of exact arguments and their mapping. Let’s look at a function that applies an event to an event handler. E.g. we have a MouseEvent and want to call a MouseEventHandler with it. Same for keyboard events, etc. If we use conditionals and union types to map event and handler, we might end up with something like this:

// All the possible event handlers
type Handler =
MouseEventHandler<HTMLButtonElement> |
KeyboardEventHandler<HTMLButtonElement>;

// Map Handler to Event
type Ev<T> =
T extends MouseEventHandler<infer R> ? MouseEvent<R> :
T extends KeyboardEventHandler<infer R> ? KeyboardEvent<R> : never;

// Create a
function apply<T extends Handler>(handler: T, ev: Ev<T>): void {
handler(ev as any); // We need the assertion here
}

At a first glance, this looks fine. It might be a bit cumbersome though if you think about all the variants that you need to keep track of.

There’s a bigger problem, though. The way TypeScript deals with all possible variants of event is causing an unexpected intersection. This means that in the function body, TypeScript can’t tell what kind of handler you are passing. Therefore it also can’t tell which kind of event we’re getting. So TypeScript says that the event can be both. A mouse event, and a keyboard event. You need to pass handlers that can deal with both. Which is not how we intend our function to work.

The actual error message is TS 2345: Argument of type ‘KeyboardEvent | MouseEvent<HTMLButtonElement, MouseEvent>’ is not assignable to parameter of type ‘MouseEvent<HTMLButtonElement, MouseEvent> & KeyboardEvent’.

This is why we need an as any type assertion. Just to make it possible to actually call the handler with the event.

So, the function signature works in a lot of scenarios:

declare const mouseHandler: MouseEventHandler<HTMLButtonElement>;
declare const mouseEv: MouseEvent<HTMLButtonElement>
declare const keyboardHandler: KeyboardEventHandler<HTMLButtonElement>;
declare const keyboardEv: KeyboardEvent<HTMLButtonElement>;

apply(mouseHandler, mouseEv); // yeah!
apply(keyboardHandler, keyboardEv) // cool!
apply(mouseHandler, keyboardEv) // 💥breaks like it should!

But once there’s ambiguity, stuff doesn’t work out as it should:

declare const mouseOrKeyboardHandler:
MouseEventHandler<HTMLButtonElement> |
KeyboardEventHandler<HTMLButtonElement>;;

// No wait, this can cause problems!
apply(mouseOrKeyboardHandler, mouseEv);

When mouseOrKeyboardHandler is a keyboard handler, we can’t reasonably pass a mouse event. Wait a second. This is exactly what the TS2345 error from above tried to tell us! We just shifted the problem to another place and made it silent with an as any assertion. Oh no!

Explicit, exact function signatures make everything easier. The mapping becomes clearer, the type signatures easier to understand, and there’s no need for conditionals or unions.

// Overload 1: MouseEventHandler and MouseEvent
function apply(
handler: MouseEventHandler<HTMLButtonElement>,
ev: MouseEvent<HTMLButtonElement>): void
// Overload 2: KeyboardEventHandler and KeyboardEvent
function apply(
handler: KeyboardEventHandler<HTMLButtonElement>,
ev: KeyboardEvent<HTMLButtonElement>): void
// The implementation. Fall back to any. This is not a type!
// TypeScript won't check for this line nor
// will it show in the autocomplete.
//This is just for you to implement your stuff.
function apply(handler: any, ev: any): void {
handler(ev);
}

Function overloads help us with all possible scenarios. We basically make sure that there no ambiguous types:

apply(mouseHandler, mouseEv); // yeah!
apply(keyboardHandler, keyboardEv) // cool!
apply(mouseHandler, keyboardEv) // 💥 breaks like it should!
apply(mouseOrKeyboardHandler, mouseEv); // 💥 breaks like it should

For the implementation, we can even use any. This is not a type seen by TypeScript, this is just for you to implement your stuff. Since you can make sure that you won’t run into a situation that implies ambiguity, we can rely on the happy-go-lucky type and don’t need to bother.

Bottom line #

Function overloads are still very useful and for a lot of scenarios the way to go. They’re easier to read, easier to write, and in a lot of cases more exact than what we get with other means.

But it’s not either-or. You can happily mix and match conditionals and function overloads if your scenario needs it. As always, here are some playgrounds:

Related Articles