Ruan Mendes
Ruan Mendes

Reputation: 92314

How to create a Partial-like that requires a single property to be set

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

Answers (8)

gafi
gafi

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

jcalz
jcalz

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.
  • 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, 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

Devin Rhode
Devin Rhode

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

aegatlin
aegatlin

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

cefn
cefn

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

jgu7man
jgu7man

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

Teodoro
Teodoro

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

cevek
cevek

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

Related Questions