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

推荐订阅源

S
Securelist
C
CERT Recently Published Vulnerability Notes
Forbes - Security
Forbes - Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
L
LINUX DO - 最新话题
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
SecWiki News
SecWiki News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Last Watchdog
The Last Watchdog
S
Schneier on Security
T
Troy Hunt's Blog
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
T
Tor Project blog
T
Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
A
Arctic Wolf
S
Secure Thoughts
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Latest
Security Latest
Scott Helme
Scott Helme
Security Archives - TechRepublic
Security Archives - TechRepublic
Latest news
Latest news
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
LINUX DO - 热门话题
P
Palo Alto Networks Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V2EX - 技术
V2EX - 技术
Google DeepMind News
Google DeepMind News
Project Zero
Project Zero
V
Vulnerabilities – Threatpost
T
Threat Research - Cisco Blogs
Webroot Blog
Webroot Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
H
Hacker News: Front Page
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog

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 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: Low maintenance types
2021-01-20 · via oida.dev | TypeScript, Rust

I write a lot about TypeScript and I enjoy the benefits it gives me in my daily work a lot. But I have a confession to make, I don’t really like writing types or type annotations. I’m really happy that TypeScript can infer so much out of my usage when writing regular JavaScript so I’m not bothered writing anything extra.

That’s how I write TypeScript in general: I write regular JavaScript, and where TypeScript needs extra information, I happily add some extra annotations. One condition: I don’t want to be bothered maintaining types. I rather create types that can update themselves if their dependencies or surroundings change. I call this approach creating low maintenance types.

Scenario 1: Information is already available #

Let’s take a look at this brief and possibly incomplete copy function. I want to copy files from one directory to another. To make my life easier, I created a set of default options so I don’t have to repeat myself too much:

const defaultOptions = {
from: "./src",
to: "./dest",
};

function copy(options) {
// Let's merge default options and options
const allOptions = { ...defaultOptions, ...options};

// todo: Implementation of the rest
}

That’s a pattern you might see a lot in JavaScript. What you see immediately is that TypeScript misses some type information. Especially the options argument of the copy function is any at the moment. So let’s better add a type for that!

One thing I could do is creating types explicitly:

type Options = {
from: string;
to: string;
};

const defaultOptions: Options = {
from: "./src",
to: "./dest",
};

type PartialOptions = {
from?: string;
to?: string;
};

function copy(options: PartialOptions) {
// Let's merge default options and options
const allOptions = { ...defaultOptions, ...options};

// todo: Implementation of the rest
}

That’s a very reasonable approach. You think about types, then you assign types, and then you get all the editor feedback and type checking you are used to. But what if something changes? Let’s assume we add another field to Options, we would have to adapt our code three times:

type Options = {
from: string;
to: string;
+ overwrite: boolean;
};

const defaultOptions: Options = {
from: "./src",
to: "./dest",
+ overwrite: true,
};

type PartialOptions = {
from?: string;
to?: string;
+ overwrite?: boolean;
};

But why? The information is already there! In defaultOptions, we tell TypeScript exactly what we’re looking for. Let’s optimize.

  1. Drop the PartialOptions type and use the utility type Partial<T> to get the same effect. You might have guessed this one already
  2. Make use of the typeof operator in TypeScript to create a new type on the fly.
const defaultOptions = {
from: "./src",
to: "./dest",
overwrite: true,
};

function copy(options: Partial<typeof defaultOptions>) {
// Let's merge default options and options
const allOptions = { ...defaultOptions, ...options};

// todo: Implementation of the rest
}

There you go. Just annotation where we need to tell TypeScript what we’re looking for.

  • If we add new fields, we don’t have to maintain anything at all
  • If we rename a field, we get just the information we care about: All usages of copy where we have to change the options we pass to the function
  • We have one single source of truth: The actual defaultOptions object. This is the object that counts because it’s the only information we have at run-time.

And our code becomes a little bit terser. TypeScript becomes less intrusive and more aligned to how we write JavaScript.

David pointed out another example that falls into this category. With the const context, typeof and index access operators, you are able to convert a tuple into a union:

const categories = [
"beginner",
"intermediate",
"advanced",
] as const;

// "beginner" | "intermediate" | "advanced"
type Category = (typeof categories)[number]

Again, we maintain just one piece, the actual data. We convert categories into a tuple type and index each element. Nice!

Scenario 2: Connected Models #

I’m not against laying out your models, though. On the contrary, I think in most cases it makes sense to be explicit and intentional about your models and your data. Let’s take a look at this toy shop:

type ToyBase = {
name: string;
price: number;
quantity: number;
minimumAge: number;
};

type BoardGame = ToyBase & {
kind: "boardgame";
players: number;
}

type Puzzle = ToyBase & {
kind: "puzzle";
pieces: number;
}

type Doll = ToyBase & {
kind: "doll";
material: "plastic" | "plush";
}

type Toy = BoardGame | Puzzle | Doll;

That’s some great data modelling here. We have a proper ToyBase which includes all properties that are available with all the distinct toy types like BoardGame, Puzzle, and Doll. With the kind attribute we can create a distinct union type Toy where we can differentiate properly:

function printToy(toy: Toy) {
switch(toy.kind) {
case "boardgame":
// todo
break;
case "puzzle":
// todo
break;
case "doll":
// todo
break;
default:
console.log(toy);
}
}

If we need the information of those models in different scenarios, we might end up with more types:

type ToyKind = "boardgame" | "puzzle" | "doll";

type GroupedToys = {
boardgame: Toy[];
puzzle: Toy[];
doll: Toy[];
};

And this is where maintenance starts again. The moment we add a type VideoGame:

type VideoGame = ToyBase & {
kind: "videogame";
system: "NES" | "SNES" | "Mega Drive" | "There are no more consoles";
};

We have to maintain at three different spots:

- type Toy = BoardGame | Puzzle | Doll;
+ type Toy = BoardGame | Puzzle | Doll | VideoGame;

- type ToyKind = "boardgame" | "puzzle" | "doll";
+ type ToyKind = "boardgame" | "puzzle" | "doll" | "videogame";

type GroupedToys = {
boardgame: Toy[];
puzzle: Toy[];
doll: Toy[];
+ videogame: Toy[];
};

This is not only a lot of maintenance but also very error-prone. Typos may happen, as I could misspell the videogame key in GroupedToys or the string "videogame" in the ToyKind union.

Let’s use some of TypeScript’s built-in functionality to change that. I think there is no reasonable way of changing the first type we need to maintain, Toy, but that’s ok. Here it’s good to be explicit because we only want to include actual toys and not something that accidentally has the same basic features.

If we want to have a union type ToyKind with all possible kind types, it’s better to not maintain them on the side, but rather access the types directly.

- type ToyKind = "boardgame" | "puzzle" | "doll";
+ type ToyKind = Toy["kind"]

That does the same trick, thanks to us creating the Toy union.

We can use the newly created and self-maintaining ToyKind type to create a new, better GroupedToys type using mapped types:

type GroupedToys = {
[Kind in ToyKind]: Toy[]
}

And that’s it! The moment we change the Toy type with new information, we have updated information in ToyKind and GroupedToys. Less to maintain for us.

We can even go further. The GroupedToys type is not exactly what we’re looking for. When we group toys, we want to make sure that we only add Doll type objects to doll, etc. So what we need to is split the union again.

The Extract type gives us a great utility to do exactly that.

// GetKind extracts all types that have the kind property set to Kind
type GetKind<Group, Kind> = Extract<Group, { kind: Kind }>

type DebugOne = GetKind<Toy, "doll"> // DebugOne = Doll
type DebugTwo = GetKind<Toy, "puzzle"> // DebugTwo = Puzzle

Let’s apply that to GroupedToys:

type GroupedToys = {
[Kind in ToyKind]: GetKind<Toy, Kind>[]
};

// this is equal to

type GroupedToys = {
boardgame: BoardGame[];
puzzle: Puzzle[];
doll: Doll[];
}

Great! Better, more correct types at no maintenance! But there’s one thing that still bugs me. The property keys. They’re singular. They should be plural:

type GroupedToys = {
[Kind in ToyKind as `${Kind}s`]: GetKind<Toy, Kind>[]
};

// this is equal to

type GroupedToys = {
boardgames: BoardGame[];
puzzles: Puzzle[];
dolls: Doll[];
}

Great! And again, no maintenance for us. The moment we change something in Toy, we get a proper update in all other types.

Defining low maintenance types #

Usually, this is my approach if I want to create low maintenance types:

  1. Model your data or infer from existing models
  2. Define derivates (mapped types, Partials, etc)
  3. Define behavior (conditionals)

I discuss the last point extensively in my book TypeScript in 50 Lessons. And, as always, enjoy the playground and fiddle around with the results.

Related Articles