typescriptunion-typesintersection-types

Ensure additional properties are present if one property is present


I want to build a type that requires certain properties to be present in case another property is present.

To do so I thought of using an intersection of union types. The union types are either a combination of some property and it's requirements or an empty type.

The following code does not work because it doesn't error out although the desc property is missing:

export type A = {
  a: string;
  desc: string;
};

export type B = {
  b: string;
} & (A | {});

const v: B = {
  b: "",
  a: "123"
};

Replacing {} by Record<string, never> results in errors because all properties are now of type never.

export type A = {
  a: string;
  desc: string;
};

export type B = {
  b: string;
} & (A | Record<string, never>);

const v: B = {
  b: "",
  a: "123"
};

Is there a way to make this concept work? Or maybe a better way?


Solution

  • Your approach is conceptually correct - you want an union of A and a type that has no common keys with A.

    Unfortunately sth akin to (A | {}) won't work - TS types allow extra properties.

    TS has excess property check for object literals, but it does not work {} unions.

    const foo: {} = {a: 1}
    

    ts-eslint even has a rule preventing its use:

    Don't use `{}` as a type. `{}` actually means "any non-nullish value".
    - If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
    - If you want a type meaning "any value", you probably want `unknown` instead.
    - If you want a type meaning "empty object", you probably want `Record<string, never>` 
    

    A | Record<string, never> works fine to alone, but, as you noticed, does not work as a part of an intersection with {b: string;} - Record forces b to be never.

    You can use following type that expresses A or sth having no keys of A

    export type A = {
      a: string;
      desc: string;
    };
    
    type NoneOf<T> = {
        [K in keyof T]?: never
    }
    
    export type B = {
      b: string;
    } & (A | NoneOf<A>);
    
    const v: B = {
      b: "",
      a: "123"
    };
    

    There is related request for Exact Types #12936 - unfortunately not implemented for years.