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

TypescriptGenerics

Typescript Problem Overview


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 this 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?

Typescript Solutions


Solution 1 - Typescript

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!

Solution 2 - Typescript

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'>.

Solution 3 - Typescript

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

Solution 4 - Typescript

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);

Solution 5 - Typescript

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]

Solution 6 - Typescript

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 }


Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJuan MendesView Question on Stackoverflow
Solution 1 - TypescriptjcalzView Answer on Stackoverflow
Solution 2 - TypescriptaegatlinView Answer on Stackoverflow
Solution 3 - TypescriptgafiView Answer on Stackoverflow
Solution 4 - TypescriptcevekView Answer on Stackoverflow
Solution 5 - TypescriptTeodoroView Answer on Stackoverflow
Solution 6 - Typescriptjgu7manView Answer on Stackoverflow