typescripttype-definition

Typescript: type with one requiered property from list of optional


I have interface with list of optional properties.

export interface OptionalIds {
  entityA_Id?: number;
  entityB_Id?: number;
  entityC_Id?: number;
}

And I have a requirement that EXACTLY one of them MUST be defined. Something like that:

export interface RequiredBId {
  entityA_Id?: undefined;
  entityB_Id: number;
  entityC_Id?: undefined;
}

export interface RequiredCId {
  entityA_Id?: undefined;
  entityB_Id?: undefined;
  entityC_Id: number;
}

export interface OptionalIds {
  entityA_Id?: number;
  entityB_Id?: number;
  entityC_Id?: number;
}

export type RestrictedOptionalIds = OptionalIds & (RequiredAId | RequiredBId | RequiredCId)

The question is: Is there other way to achieve described behaviour without weird constructions?


Solution

  • Updated Answer

    Thanks to this post for RequireOnlyOne: https://stackoverflow.com/a/49725198/4529555

    type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
        Pick<T, Exclude<keyof T, Keys>>
        & {
            [K in Keys]-?:
                Required<Pick<T, K>>
                & Partial<Record<Exclude<Keys, K>, undefined>>
        }[Keys]
    
    export interface OptionalIds {
      entityA_Id?: number;
      entityB_Id?: number;
      entityC_Id?: number;
    }
    
    const exampleA: RequireOnlyOne<OptionalIds> = {
      entityA_Id: 1
    }
    const exampleB: RequireOnlyOne<OptionalIds> = {
      entityB_Id: 1
    }
    
    const exampleC: RequireOnlyOne<OptionalIds> = {
      entityC_Id: 1
    }
    
    // Error
    const exampleMultiple: RequireOnlyOne<OptionalIds> = {
      entityA_Id: 1,
      entityB_Id: 2,
    }
    
    // Error: {} not assignable to RequireAtLeastOne<OptionalIds, keyof OptionalIds>
    const exampleTsError: RequireOnlyOne<OptionalIds> = {
    
    }
    

    Original Answer

    Thanks to this post for RequireAtLeastOne: https://stackoverflow.com/a/49725198/4529555

    type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
        Pick<T, Exclude<keyof T, Keys>> 
        & {
            [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
        }[Keys]
    
    export interface OptionalIds {
      entityA_Id?: number;
      entityB_Id?: number;
      entityC_Id?: number;
    }
    
    const exampleA: RequireAtLeastOne<OptionalIds> = {
      entityA_Id: 1
    }
    const exampleB: RequireAtLeastOne<OptionalIds> = {
      entityB_Id: 1
    }
    
    const exampleC: RequireAtLeastOne<OptionalIds> = {
      entityC_Id: 1
    }
    
    const exampleMultiple: RequireAtLeastOne<OptionalIds> = {
      entityA_Id: 1,
      entityB_Id: 2
    }
    
    // Error: {} not assignable to RequireAtLeastOne<OptionalIds, keyof OptionalIds>
    const exampleTsError: RequireAtLeastOne<OptionalIds> = {
    
    }