Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript type which requries exactly one property to be undefined

I have a type for which I want to be able to have exactly one property undefined.

For example, if I had

type MyType = {
  a: number;
  b: number;
  c: number;
};

I would expect OneUndefined<MyType>

to be

  | {
      a?: undefined;
      b: number;
      c: number;
    }
  | {
      a: number;
      b?: undefined;
      c: number;
    }
  | {
      a: number;
      b: number;
      c?: undefined;
    };

Would also be interested in the inverse, OneDefined<T>, or NDefined<T, n>, where exactly n properties are defined.

I have tried forming a type based on similar questions regarding a certain number of properties:

https://stackoverflow.com/a/49725198

https://stackoverflow.com/a/62163715/10252820

However have been unable to make these fit the question being asked here.

like image 465
Ted Avatar asked Oct 31 '25 10:10

Ted


2 Answers

Ok I managed to come to an answer for the case of OneUndefined<T>:

type OneUndefined<T> = {
  [K in keyof T]: {
    [P in K]?: undefined;
  } & Omit<T, K>;
}[keyof T];

type MyType = {
  a: number;
  b: number;
  c: number;
};

// valid
const valid1: OneUndefined<MyType> = {
  a: 1,
  b: undefined,
  c: 3,
};

// valid
const valid2: OneUndefined<MyType> = {
  a: 1,
  b: 2,
};

// rejected
const invalid1: OneUndefined<MyType> = {
  a: 1,
  b: 2,
  c: 3,
};

// rejected
const invalid2: OneUndefined<MyType> = {
  a: 1,
  b: undefined,
  c: undefined,
};

For MyType

This section:

  [K in keyof T]: {
    [P in K]?: undefined;
  } & Omit<T, K>;

Generates this:

{
  a: {
    a?: undefined;
    b: number;
    c: number;
  };
  b: {
    a: number;
    b?: undefined;
    c: number;
  };
  c: {
    a: number;
    b: number;
    c?: undefined;
  };
};

Then this }[keyof T]; picks one of the props from the type we just generated.

Edit: OneDefined<T> is similar:

type OneDefined<T> = {
  [K in keyof T]: {
    [P in K]: T[P];
  } & { [P in keyof Omit<T, K>]?: undefined };
}[keyof T];
like image 83
Ted Avatar answered Nov 02 '25 02:11

Ted


I managed to come up with an NDefined<T, N> approach using recursive generic types.

type MyType = {
  a: number;
  b: number;
  c: number;
};

type NDefined<
  T extends Record<PropertyKey, unknown>,
  N extends number,
  R extends (keyof T)[] = [],
> = N extends R["length"]
  ? HasDuplicates<R> extends true
    ? never
    : Partial<T> & Required<Pick<T, R[number]>>
  : {
      [K in keyof T]: NDefined<T, N, [...R, K]>;
    }[keyof T];
type HasDuplicates<T extends unknown[]> = T extends [infer L, ...infer R]
  ? L extends R[number]
    ? true
    : HasDuplicates<R>
  : false;

type Result = NDefined<MyType, 2>;
//   ^? type Result = (Partial<MyType> & Required<Pick<MyType, "a" | "b">>)
//                  | (Partial<MyType> & Required<Pick<MyType, "a" | "c">>)
//                  | (Partial<MyType> & Required<Pick<MyType, "b" | "c">>)

TypeScript Playground


First, compute all possible permutations of keyof T as tuples with length N. E. g. with NDefined<{a: any, b: any, c: any}, 2> we get ["a", "a"] | ["a", "b"] | ["a", "c"] | ["b", "a"] | ["b", "b"] | ["b", "c"] | ["c", "a"] | ["c", "b"] | ["c", "c"]. These tuples mark all the required keys. Tuples that contain duplicates such as ["a", "a"] can be detected with the following helper type (inspired by this answer).

type HasDuplicates<T extends unknown[]> = T extends [infer L, ...infer R]
  ? L extends R[number]
    ? true
    : HasDuplicates<R>
  : false;

It's a bit unelegant to use an extra type for this yet I couldn't find a way to prevent such duplicates directly in the recursive call without getting circular reference errors.

Finally, we can compute the valid object (Record) type permutations with Partial<T> & Required<Pick<T, R[number]>>. This intersection type essentially marks all keys as optional (Partial) except for the tuple keys subsets that we transform into a union using indexed access types and then mark as Required. This works due to distributive conditional types.

like image 22
Behemoth Avatar answered Nov 02 '25 00:11

Behemoth



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!