3

I'm pretty sure its my lack of experience utilizing complex generics so I'm hoping someone has ideas on how to accomplish this. My use case is for creating "form types" for my React/Formik form values without having to either re-type a new definition or pass in full objects with lots of non-updatable properties.

I found this answer which shows how to exclude readonly properties from a TypeScript type, but I'm finding it hard to wrap my brain around making it recursive. Additionally, I'd love to have it omit properties that return empty nested objects (if possible).

How to exclude getter only properties from type in typescript

type IfEquals<X, Y, A=X, B=never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

type WritableKeys<T> = {
  [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T];

type ReadonlyKeys<T> = {
  [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
}[keyof T];

type Writable<T> = Pick<T, WritableKeys<T>>

type Brand = {
  readonly id: number;
  name: string;
  logo: {
    readonly id: number;
    url: string;
  },
  creator: {
    readonly id: number;
  }
};

type EditableBrand = Writable<Brand>;
// type EditableBrand = {
//   name: string;
//   logo: {
//     url: string;
//   }
// };
ericchernuka
  • 155
  • 10

1 Answers1

4

There's a suggestion for a DeepReadonly<T> type in TypeScript repo and some suggested implementations as well. And the same question on StackOverflow. Here. is an example of how DeepReadonly is implemented. You can use the same technique to implement DeepWritable type:

type IfEquals<X, Y, A=X, B=never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

type WritableKeys<T> = {
  [P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T];

type DeepWritablePrimitive = undefined | null | boolean | string | number | Function;
type DeepWritable<T> =
    T extends DeepWritablePrimitive ? T :
    T extends Array<infer U> ? DeepWritableArray<U> :
    T extends Map<infer K, infer V> ? DeepWritableMap<K, V> :
    T extends Set<infer T> ? DeepWriableSet<T> : DeepWritableObject<T>;

type DeepWritableArray<T> = Array<DeepWritable<T>>;
type DeepWritableMap<K, V> = Map<K, DeepWritable<V>>;
type DeepWriableSet<T> = Set<DeepWritable<T>>;

type DeepWritableObject<T> = {
    [K in WritableKeys<T>]: DeepWritable<T[K]>
};

Playground


And lets extend the type to omit keys that return empty nested objects:

...

type EmptyKeys<T> = {
  [P in keyof T]: {} extends T[P] ? P : never
}[keyof T];

type OmitEmptyKeys<T> = Omit<T, EmptyKeys<T>>;
type DeepWritable<T> = ... : OmitEmptyKeys<DeepWritableObject<T>>;

Playground

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123