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

推荐订阅源

F
Full Disclosure
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
The GitHub Blog
The GitHub Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recorded Future
Recorded Future
Y
Y Combinator Blog
Cloudbric
Cloudbric
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cybersecurity and Infrastructure Security Agency CISA
TaoSecurity Blog
TaoSecurity Blog
Security Latest
Security Latest
N
News and Events Feed by Topic
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
DataBreaches.Net
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Hacker News: Front Page
C
Cisco Blogs
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V
Vulnerabilities – Threatpost
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
A
About on SuperTechFans
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Jina AI
Jina AI
C
CXSECURITY Database RSS Feed - CXSecurity.com
Schneier on Security
Schneier on Security
T
Tenable Blog
N
News and Events Feed by Topic
W
WeLiveSecurity
有赞技术团队
有赞技术团队
AI
AI
爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
T
The Blog of Author Tim Ferriss
S
Security Affairs
Know Your Adversary
Know Your Adversary
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
Google DeepMind News
Google DeepMind News
The Cloudflare Blog

Kent C. Dodds Blog

Implementing Hybrid Semantic + Lexical Search Simplifying Containers with Cloudflare Sandboxes Migrating to Workspaces and Nx Offloading FFmpeg with Cloudflare Building Semantic Search on my Content Helping YOU ask ME questions with AI How I used Cursor to Migrate Frameworks The Dow's Start on the Covenant Path 2025 in Review The next chapter: EpicAI.pro AI is taking your job How I increased my visibility Launching Epic Web 2023 in Review Stop Being a Junior RSC with Dan Abramov and Joe Savona Live Stream Fixing a Memory Leak in a Production Node.js App 2022 in Review My Car Accident I Migrated from a Postgres Cluster to Distributed SQLite with LiteFS I'm building EpicWeb.dev A review of my time at Remix Remix: The Yang to React's Yin How I help you build better websites Why I Love Remix The State Initializer Pattern How to React ⚛️ Get a catch block error message with TypeScript Building an awesome image loading experience How Remix makes CSS clashes predictable Introducing the new kentcdodds.com How I built a modern website in 2021 How to use React Context effectively Static vs Unit vs Integration vs E2E Testing for Frontend Apps The Testing Trophy and Testing Classifications Array reduce vs chaining vs for loop Don't Solve Problems, Eliminate Them Super Simple Start to Remix Super Simple Start to ESModules in Node.js JavaScript Pass By Value Function Parameters How to optimize your context value How to write a React Component in TypeScript TypeScript Function Syntaxes Listify a JavaScript Array Build vs Buy: Component Libraries edition Using fetch with TypeScript Wrapping React.useState with TypeScript Define function overload types with TypeScript 2020 in Review Business and Engineering alignment Hi, thanks for reaching out to me 👋 useEffect vs useLayoutEffect Super simple start to Firebase functions Super simple start to Netlify functions Super Simple Start to css variables Favor Progress Over Pride in Open Source Testing Implementation Details How getting into Open Source has been awesome for me useState lazy initialization and function updates Use ternaries rather than && in JSX Application State Management with React Use react-error-boundary to handle errors in React JavaScript to Know for React How I structure Express apps What open source project should I contribute to? When I follow TDD AHA Programming 💡 How I Record Educational Videos Should I write a test or fix a bug? Stop mocking fetch Intentional Career Building Improve test error messages of your abstractions Tracing user interactions with React Eliminate an entire category of bugs with a few simple tools Common mistakes with React Testing Library Super Simple Start to React Stop using client-side route redirects The State Reducer Pattern with React Hooks Function forms Replace axios with a simple custom fetch wrapper How to test custom React hooks React Production Performance Monitoring Should I useState or useReducer? Stop using isLoading booleans Make Your Test Fail Make your own DevTools An Argument for Automation Fix the "not wrapped in act(...)" warning Super Simple Start to ESModules in the Browser Implementing a simple state machine library in JavaScript 2010s Decade in Review Why users care about how you write code Why I avoid nesting closures Don't call a React function component Why your team needs TestingJavaScript.com Inversion of Control Understanding React's key prop How to Enable React Concurrent Mode How to add testing to an existing project Profile a React App for Performance
How to write a Constrained Identity Function (CIF) in TypeScript
2021-03-09 · via Kent C. Dodds Blog

In How to write a React Component in TypeScript, I typed an example React component. Here's where we left off:

const operations = {
	'+': (left: number, right: number): number => left + right,
	'-': (left: number, right: number): number => left - right,
	'*': (left: number, right: number): number => left * right,
	'/': (left: number, right: number): number => left / right,
}

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}

function Calculator({ left, operator, right }: CalculatorProps) {
	const result = operations[operator](left, right)
	return (
		<div>
			<code>
				{left} {operator} {right} = <output>{result}</output>
			</code>
		</div>
	)
}

const examples = (
	<>
		<Calculator left={1} operator="+" right={2} />
		<Calculator left={1} operator="-" right={2} />
		<Calculator left={1} operator="*" right={2} />
		<Calculator left={1} operator="/" right={2} />
	</>
)

I'm not satisfied with the operations function though. I know that every function in that object is going to have the exact same type (by necessity due to the use case):

type OperationFn = (left: number, right: number) => number

The operations object is really just a record of operation strings mapped to a function that operates on two numbers. So if we add a type annotation on our operations variable, then we don't have to type each function individually. Let's try that:

type OperationFn = (left: number, right: number) => number
const operations: Record<string, OperationFn> = {
	'+': (left, right) => left + right,
	'-': (left, right) => left - right,
	'*': (left, right) => left * right,
	'/': (left, right) => left / right,
}

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}

Sweet, so we don't have to type every function individually, but oh no... now the typeof operations is Record<string, OperationFn> and the keyof of that is going to be string which means our CalculatorProps['operator'] type will be string. Ugh 😩

Here's what we could do to fix this:

type OperationFn = (left: number, right: number) => number
type Operator = '+' | '-' | '/' | '*'
const operations: Record<Operator, OperationFn> = {
	'+': (left, right) => left + right,
	'-': (left, right) => left - right,
	'*': (left, right) => left * right,
	'/': (left, right) => left / right,
}

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}

But now we're back to having to add ** in two places if we decide to add the Exponentiation operator. However, in this case, TypeScript will give us a compiler error if we add it in one and not the other, so that's a step up.

This is where I left this when I first wrote this component, but then @AlekseyL13 suggested that I try a properly typed identity function.

The constrained identity function

First, let's keep in mind, we have 2 goals:

  1. Enforce that the type of each property is the same (in this simple example, it's just a number, but in our actual example, it's a function type)
  2. Ensure that keyof typeof for our object results in a finite union of the keys

TypeScript version 4.9.0 introduces satisfies which ... eh... satisfies our use cases here. Please feel free to skip to the end if you're using TypeScript v4.9.0 or greater.

With TypeScript, it's a challenge to have both of these. By default, we get the second goal. The problem is that when you try to accomplish the first goal with a type annotation like const operations: Record<string, OperationFn> = ..., you end up widening the key so keyof typeof results in string. Ugh, how annoying.

So here's where the constrained identity function comes in. By the way, "constrained" describes a situation where you have a function that accepts a narrower version of an input than it's passed.Here's a simple example:

type NamedObject = { name: string }
function getUserName<User extends NamedObject>(user: User) {
	return user.name
}

const obj = { name: 'Hannah', age: 3 }
getUserName(obj)

So the object that's passed to getUserName must satisfy all the types in the NamedObject. The getUserName constrains the input to at least match that type.

And an "identity function" is a function that accepts a value and returns that value. I sometimes use these kinds of functions as the default value for callbacks:

const identity = <Type extends unknown>(item: Type) => item

type ModifyConfigFn = (config: ConfigType) => ConfigType
function buildProject(modifyConfig: ModifyConfigFn = identity) {
	const config: ConfigType = {
		/* some config */
	}
	const modifiedConfig = modifyConfig(config)
	// more stuff...
}

So with those definitions out of the way, a "constrained identity function" is a function which returns what it is given and also helps TypeScript constrain its type. This is exactly what we want to do.

We can call it a CIF (pronounced "see eye eff"). Sure, let's go with that.

Let me show you a simple example first, then I'll explain what's going on, then we can apply it more usefully to our more complicated example:

type Value = number
const createNumbers = <ObjectType extends Record<string, Value>>(
	obj: ObjectType,
) => obj

const numbers = createNumbers({ one: 1, two: 2, three: 3, four: 4 })

// @ts-expect-error we don't have 'five' yet
numbers['five']

So the createNumbers is the constrained identity function. It returns the obj it's given, hopefully that's clear. But how does it enforce our input and constrain the type?

Let me explain it this way. If we start with:

const numbers = { one: 1, two: 2, three: 3, four: 4 }
// typeof numbers:
// {
//   one: number;
//   two: number;
//   three: number;
//   four: number;
// }

But in the future, someone could come to this code and change it like this:

const numbers = { one: 1, two: 2, three: 3, four: 4, five: '5' }
// typeof numbers:
// {
//   one: number;
//   two: number;
//   three: number;
//   four: number;
//   five: string; // 😱
// }

Yikes! Nah, we can't have that! (And, more importantly, in our Calculator example, some auto-typing on the functions is the goal here).

So, let's enforce our value types with a type annotation:

// @ts-expect-error HA! We gotcha! No strings in this object!
const numbers: Record<string, number> = {
	one: 1,
	two: 2,
	three: 3,
	four: 4,
	five: '5',
}

But now by typing our values explicitly, we've told TypeScript that our key can be a string. Unfortunately, there's no way to tell TypeScript: "This thing has the keys it has, but the values are this specific type." IMO, this is a missing feature of TypeScript. Our createNumbers constrained identity function (er... "CIF") is a workaround.

So here's what that workaround is:

Constrained identity functions allow us to not explicitly annotate our variable while still getting to enforce the values.

So we create the object, get the best type that TypeScript can offer us (which includes the narrow keys and wide values), then we pass it to a function which accepts wide keys and narrow values. TypeScript combines that to give us a Record with a key and value which are both narrow!

Alrighty, so let's apply a CIF to our original situation:

type OperationFn = (left: number, right: number) => number
const createOperations = <OperationsType extends Record<string, OperationFn>>(
	operations: OperationsType,
) => operations

const operations = createOperations({
	'+': (left, right) => left + right,
	'-': (left, right) => left - right,
	'*': (left, right) => left * right,
	'/': (left, right) => left / right,
})

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}

// @ts-expect-error we haven't added support
// for the exponentiation operator yet
operations['**'](1, 2)

Wahoo! So with this solution we don't have to explicitly type all the operation functions the exact same way and we can still get a union type of all available operations.

A generic CIF?

You may have noticed that we had two CIFs in the previous section:

type Value = number
const createNumbers = <ObjectType extends Record<string, Value>>(
	obj: ObjectType,
) => obj

type OperationFn = (left: number, right: number) => number
const createOperations = <OperationsType extends Record<string, OperationFn>>(
	operations: OperationsType,
) => operations

Wouldn't it be neat if we could combine those? Sure would. But you're not going to like it... Here's what I tried first:

const constrain = <Given, Inferred extends Given>(item: Inferred) => item

// @ts-expect-error Expected 2 type arguments, but got 1.(2558)
const numbers = constrain<Record<string, number>>({ one: 1 /* etc. */ })

Sad day. Unfortunately this is just not possible with TypeScript today. But here's a workaround:

const constrain =
	<Given extends unknown>() =>
	<Inferred extends Given>(item: Inferred) =>
		item

const numbers = constrain<Record<string, number>>()({ one: 1 /* etc. */ })

... yeah, I told you you wouldn't like it. It's marginally better like this:

const createNumbers = constrain<Record<string, number>>()
const numbers = createNumbers({ one: 1 /* etc. */ })

But like, huh. Bummer.

Luckily, I don't find myself making CIFs very often anyway and they aren't difficult to write so I don't need an abstraction for them. Thought it'd be interesting to share with you though 😄

TypeScript 4.9.0 - satisfies

With the satisfies keyword in TypeScript, you can avoid all these issues pretty easily:

type OperationFn = (left: number, right: number) => number

const operations = {
	'+': (left, right) => left + right,
	'-': (left, right) => left - right,
	'*': (left, right) => left * right,
	'/': (left, right) => left / right,
} satisfies Record<string, OperationFn>

This effectively does the same thing and has none of the drawbacks. Hooray for progress!

Conclusion

Here's the final version of our calculator component with everything typed with our CIF:

type OperationFn = (left: number, right: number) => number
const createOperations = <OperationsType extends Record<string, OperationFn>>(
	opts: OperationsType,
) => opts

const operations = createOperations({
	'+': (left, right) => left + right,
	'-': (left, right) => left - right,
	'*': (left, right) => left * right,
	'/': (left, right) => left / right,
})

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}
function Calculator({ left, operator, right }: CalculatorProps) {
	const result = operations[operator](left, right)
	return (
		<div>
			<code>
				{left} {operator} {right} = <output>{result}</output>
			</code>
		</div>
	)
}

const examples = (
	<>
		<Calculator left={1} operator="+" right={2} />
		<Calculator left={1} operator="-" right={2} />
		<Calculator left={1} operator="*" right={2} />
		<Calculator left={1} operator="/" right={2} />
	</>
)

Yup, this entire blog post was written just to explain those 3 lines of code to you. So yeah, there you go.

Some folks may finish reading this post and scoff, saying things like: "Why would you ever want to use TypeScript if it requires you to do weird things like this?"

First, I'd say that just because a tool like TypeScript requires workarounds for some stuff like this doesn't mean it's not worthwhile. The cost here is minimal and the benefit is significant. I'm not here to convince you to use TypeScript. I can't do as good a job convincing you as your runtime bugs do I'm sure 😜 Secondly, this is definitely something that could improve with TypeScript in the future. In fact, this may be a nice step to improving things. Finally, like I said, this isn't something that we're doing all the time. Most of my time with TypeScript is delightful.

Take care!