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> : t
In 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