How to create a Partial-like that requires a single property to be set
I think I have a solution for you. You're looking for something that takes a type T
and produces a related type which contains at least one property from T
. That is, it's like Partial<T>
but excludes the empty object.
If so, here it is:
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
To dissect it: first of all, AtLeastOne<T>
is Partial<T>
intersected with something. U[keyof U]
means that it's the union of all property values of U
. And I've defined (the default value of) U
to be a mapped type where each property of T
is mapped to Pick<T, K>
, a single-property type for the key K
. (For example, Pick<{foo: string, bar: number},'foo'>
is equivalent to {foo: string}
... it "picks" the 'foo'
property from the original type.) Meaning that U[keyof U]
in this case is the union of all possible single-property types from T
.
Hmm, that might be confusing. Let's see step-by-step how it operates on the following concrete type:
type FullLinkRestSource = { model: string; rel: string; title: string;}type LinkRestSource = AtLeastOne<FullLinkRestSource>
That expands to
type LinkRestSource = AtLeastOne<FullLinkRestSource, { [K in keyof FullLinkRestSource]: Pick<FullLinkRestSource, K>}>
or
type LinkRestSource = AtLeastOne<FullLinkRestSource, { model: Pick<FullLinkRestSource, 'model'>, rel: Pick<FullLinkRestSource, 'rel'>, title: Pick<FullLinkRestSource, 'title'>}>
or
type LinkRestSource = AtLeastOne<FullLinkRestSource, { model: {model: string}, rel: {rel: string}, title: {title: string}>}>
or
type LinkRestSource = Partial<FullLinkRestSource> & { model: {model: string}, rel: {rel: string}, title: {title: string}>}[keyof { model: {model: string}, rel: {rel: string}, title: {title: string}>}]
or
type LinkRestSource = Partial<FullLinkRestSource> & { model: {model: string}, rel: {rel: string}, title: {title: string}>}['model' | 'rel' | 'title']
or
type LinkRestSource = Partial<FullLinkRestSource> & ({model: string} | {rel: string} | {title: string})
or
type LinkRestSource = {model?: string, rel?: string, title?: string} & ({model: string} | {rel: string} | {title: string})
or
type LinkRestSource = { model: string, rel?: string, title?: string } | {model?: string, rel: string, title?: string} | {model?: string, rel?: string, title: string}
which is, I think, what you want.
You can test it out:
const okay0: LinkRestSource = { model: 'a', rel: 'b', title: 'c' }const okay1: LinkRestSource = { model: 'a', rel: 'b' }const okay2: LinkRestSource = { model: 'a' }const okay3: LinkRestSource = { rel: 'b' }const okay4: LinkRestSource = { title: 'c' }const error0: LinkRestSource = {} // missing propertyconst error1: LinkRestSource = { model: 'a', titel: 'c' } // excess property on string literal
So, does that work for you? Good luck!
There's another solution if you know which properties you want.
AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>
This would also allow you to lock in multiple keys of a type, e.g. AtLeast<T, 'model' | 'rel'>
.
A simpler version of the solution by jcalz:
type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]
so the whole implementation becomes
type FullLinkRestSource = { model: string; rel: string; title: string;}type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]type LinkRestSource = AtLeastOne<FullLinkRestSource>const okay0: LinkRestSource = { model: 'a', rel: 'b', title: 'c' }const okay1: LinkRestSource = { model: 'a', rel: 'b' }const okay2: LinkRestSource = { model: 'a' }const okay3: LinkRestSource = { rel: 'b' }const okay4: LinkRestSource = { title: 'c' }const error0: LinkRestSource = {} // missing propertyconst error1: LinkRestSource = { model: 'a', titel: 'c' } // excess property on string literal