Reputation: 92314
We have a structure that is like the following:
export type LinkRestSource = {
model: string;
rel?: string;
title?: string;
} | {
model?: string;
rel: string;
title?: string;
} | {
model?: string;
rel?: string;
title: string;
};
Which is almost the same as saying
type LinkRestSource = Partial<{model: string, rel: string, title: string}>
Except that the Partial
will allow an empty object to be passed in whereas the initial type requires one of the properties to be passed in.
How can I create a generic like Partial
, but that behaves like my structure above?
Upvotes: 129
Views: 87623
Reputation: 12794
A simpler version of this solution:
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', titel: 'c' } // incorrectly spelled property
and here's the TS playground link to try it
Upvotes: 26
Reputation: 329943
You're looking for something that takes a type T
and produces a related type which contains at least one property from T
. 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
.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, the following is equivalent because it "picks" the 'foo'
property from the original type:
Pick<{foo: string, bar: number},'foo'>
// is equivalent to
{foo: string}
meaning that U[keyof U]
in this case is the union of all possible single-property types from T
. 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>
which 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
Upvotes: 196
Reputation: 25367
This is an amazing post.
I almost went from a simple { model?: string, title?: string }
to pulling in this utility, but in my case, I am deciding not to.
When a user of my function goes to call it, then will do something like:
lib({
content1: {}
})
In my scenario {}
is simply "unclean", but there's nothing WRONG with it... and I'd rather not give users of the library an error as they are typing, because I'd like them to type this:
lib({
content1: { CTRL + SPACEBAR }
})
And see the options available.
Upvotes: 0
Reputation: 2061
There's another solution if you know which properties you want.
type 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.
type LinkRestSource = AtLeast<T, 'model' | 'rel'>
Upvotes: 124
Reputation: 3341
In my case I wanted at least one property to be actually set (not just drawn from the union, in which some paths had undefined
values by design).
The simplest formulation I could produce was...
type SomePropertyFrom<T> = { [K in keyof T]: Pick<Required<T>, K> }[keyof T]
I didn't find any of the terser approaches above to work, when dealing with a complex union like e.g. {concurrent:number} | {concurrent?:never}
and the more long-winded ones looked terrifying and I'd rather understand my types fully.
My approach converged on a variant ofthe solution by gafi which was type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]
but where crucially I pick from Required<T>
, else undefined
remains in the set of valid property values coming from my type unions (and it therefore still doesn't exclude the empty object).
It should be possible to use the above in isolation, but for reference my full solution for defining a non-empty ScheduleOptions
type, backed by a complex union, is shown below. This example also shows a definition for AllOrNothing<T>
, which may be a complementary type for these kinds of problems...
/** Allows ConcurrencyLimit properties, or IntervalLimit properties or both, but requires at least one group to be fully set */
type ScheduleOptions = SomeLimit & {
errorHandler?: (err: unknown) => unknown;
};
/** A limit on the number of pending promises (created but not yet settled) */
interface ConcurrencyLimit {
concurrency: number;
}
/** A limit on the number of promises created within a millisecond interval */
interface IntervalLimit {
intervalCap: number;
intervalMs: number;
}
/** Allow any limit to be set or unset (implicitly includes case of no limits set, which we will exclude in the next step) */
type AnyLimit = AllOrNothing<ConcurrencyLimit> & AllOrNothing<IntervalLimit>;
/** Require at least some limit to be set (excludes case of no limits) */
type SomeLimit = AnyLimit & SomePropertyFrom<AnyLimit>;
/** Require a type's properties to be either fully present, or fully absent */
type AllOrNothing<T> =
| T
| {
[k in keyof Required<T>]?: never;
};
/** Require at least one assigned property from T */
type SomePropertyFrom<T> = { [K in keyof T]: Pick<Required<T>, K> }[keyof T];
Upvotes: 0
Reputation: 366
Another way and if you need keep some properties required and at least one of rest required too. See Typescript Playground example.
The base interface could looks like:
export interface MainData {
name: string;
CRF: string;
email?: string;
cellphone?: string;
facebookId?: string;
}
...and if you only need at least one between 'email', 'cellphone' and 'facebookId', change and merge interfaces without optional symbol for every propoerty:
export interface registByEmail extends Omit<MainData, 'email'> { email: string }
export interface registByCellphone extends Omit<MainData, 'cellphone'> { cellphone: string }
export interface registByFacebook extends Omit<MainData, 'facebookId'> { facebookId: string }
export type RegistData = registByCellphone | registByEmail | registByFacebook
And results will looks like:
// language throws error
let client: RegistData = { name, CRF }
// its ok
let client: RegistData = { name, CRF, email }
let client: RegistData = { name, CRF, cellphone }
let client: RegistData = { name, CRF, facebookId }
let client: RegistData = { name, CRF, email, cellphone }
Upvotes: 1
Reputation: 1474
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]
Upvotes: 6
Reputation: 862
Maybe something like that:
type X<A, B, C> = (A & Partial<B> & Partial<C>) | (Partial<A> & B & Partial<C>) | (Partial<A> & Partial<B> & C);
type LinkRestSource = X<{ model: string }, { rel: string }, { title: string }>
var d: LinkRestSource = {rel: 'sdf'};
But it little bit messy :)
or
type Y<A, B, C> = Partial<A & B & C> & (A | B | C);
Upvotes: 2