Combine sets of properties using interface extensions rather than extracting them using utilities like Omit.
β
interface AllProps {
foo: string
bar: boolean
baz: number
}
type BarVariant = Omit<AllProps, "baz">
type BazVariant = Omit<AllProps, "bar">β
interface BaseProps {
foo: string
}
interface WithBar extends BaseProps {
bar: boolean
}
interface WithBaz extends BaseProps {
baz: number
}This has lots of implications when designing efficient types, including principle 3.
One that makes your life easier is you don't have to worry about duplicate instantiations.
π
import { doExpensiveThing } from "./complexTypes.ts"
type getResult<t> =
// the second doExpensiveThing<T> will not be evaluated
// the repetition can safely be ignored from a perf perspective
doExpensiveThing<t> extends SomeType ? doExpensiveThing<t> : tIn accordance with 2, generics type parameters should reflect the minimum information needed to determine the correct output.
Extra context that could be innocuous at runtime will lead to worse caching.
β
type doExpensiveThing<ctx extends BigContextObject> =
ctx["relevantKey"] extends SomeType
? // ...series of conditionals + transforms on Ctx["relevantKey"]
never
: never
type Result = doExpensiveThing<MyBigContextObject>β
type doExpensiveThing<relevantValue extends ExactlyWhatYouNeed> =
relevantValue extends SomeType
? // ...series of conditionals + transforms on RelevantValue
never
: never
type Result = doExpensiveThing<MyBigContextObject["relevantKey"]>Rather than composing a series of builtins like Partial and Omit to do what you want, write a single type that will transform the type the way you need.
β
// pick numeric keys, make keys optional, convert values to arrays
type transform<o extends object> = valuesToArrays<Pick<Partial<o>, `${number>}`>>
type valuesToArrays<o extends object> = { [k in keyof o]: o[k][] }β
// pick numeric keys, make keys optional, convert values to arrays
type transform<o extends object> = {
[k in keyof o as k extends `${number}` ? k : never]?: o[k][]
}If needed, ensure they are discriminated and avoid intersecting them with other unions
β οΈ
// inherently O(N^2)- find another way to get types that are "good enough"
type Result = LargeUnionA & LargeUnionB
// checking assignability can be similarly expensive if not discriminated
const result: LargeUnionA = getUnionB()Type instantiations are a useful heuristic for performance and can granularly captured by @ark/attest:
import { bench } from "@ark/attest"
type makeComplexType<s extends string> = s extends `${infer head}${infer tail}`
? head | tail | makeComplexType<tail>
: s
bench("makeComplexType", () => {
return {} as makeComplexType<"defenestration">
// this is an inline snapshot that will be populated or compared
// when you run the file. it reflects the number of type instantiations
// directly contributed by the body of the `bench` call, including
// function calls, assignments and type instantiations like this one.
}).types([169, "instantiations"])This can be extremely useful when optimizing the implementation of a particular type and for avoiding regressions in CI.
Docs: https://github.com/arktypeio/arktype/tree/main/ark/attest#benches
TypeScript's --generateTrace option can capture type performance trace data for your project. This is a great way to get a top-down view of type perf for a project and build intuitions about where to start optimizing.
@ark/attest's CLI supplements this with some additional analysis around the most expensive function calls in your repo.
Docs: https://github.com/arktypeio/arktype/tree/main/ark/attest#trace