Search code examples
typescriptgenerics

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?


Solution

  • 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