How to create a Partial-like that requires a single property to be set
Unfortunately the above answers didn't work for me.
Either because the compiler couldn't catch the errors or because my IDE could not retrieve the expected attributes of an object even when it's type was annotated.
The following worked perfectly, and was taken from the official microsoft azure/keyvault-certificates package:
type RequireAtLeastOne<T> = { [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>; }[keyof T]
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 property
const error1: LinkRestSource = { model: 'a', title: 'c' } // excess property on string literal
and here's the TS playground link to try it
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 property
const error1: LinkRestSource = { model: 'a', titel: 'c' } // excess property on string literal
So, does that work for you? Good luck!