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

后端 未结 5 1196
春和景丽
春和景丽 2020-11-29 03:24

We have a structure that is like the following:

export type LinkRestSource = {
    model: string;
    rel?: string;
    title?: string;
} | {
    model?: str         


        
相关标签:
5条回答
  • 2020-11-29 03:47

    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);
    
    0 讨论(0)
  • 2020-11-29 03:48

    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', titel: 'c' } // excess property on string literal
    

    and here's the TS playground link to try it

    0 讨论(0)
  • 2020-11-29 03:58

    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!

    0 讨论(0)
  • 2020-11-29 04:09

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

    0 讨论(0)
  • 2020-11-29 04:09

    That would not be possible with Partial<T>. Under the hood it looks like this:

    type Partial<T> = { [P in keyof T]?: T[P]; };
    

    All properties made optional.

    I doubt it is possible (or easy) to enforce your rule via type system.

    Could try to create type that employs keyof in a similar way, but have a condition in default constructor.

    If you can think of a way to declare a type like Partial that builds a matrix of types like yours emitting ? for a different key in each and concat all of them using | like in your first example, you might be able to enforce your rule vie type system.

    This blog post on keyof might give you some ideas.

    0 讨论(0)
提交回复
热议问题