orval icon indicating copy to clipboard operation
orval copied to clipboard

Issues with NonReadonly types and array types

Open localrobot opened this issue 2 years ago • 5 comments

When an object contains a field with an array type, the NonReadonly wrapped type cannot be assinged to an array.

For Reference (this is generated for NonReadonly 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 UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type DistributeReadOnlyOverUnions<T> = T extends any ? NonReadonly<T> : never;

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

type NonReadonly<T> = [T] extends [UnionToIntersection<T>]
  ? { [P in keyof Writable<T>]: T[P] extends object ? NonReadonly<NonNullable<T[P]>> : T[P] }
  : DistributeReadOnlyOverUnions<T>;

What are the steps to reproduce this issue?

Example:

type Name = { first: string, last: string };

type NamesObject = { names: Name[] };

const namesObj: NonReadonly<NamesObject> = { names: [{ first: 'a', last: 'b' }] }; // This works.

// Type '{ [x: number]: { first: string; last: string; }; }' is missing the following properties from
// type 'Name[]': length, pop, push, concat, and 29 more.ts(2740)
const names: Name[] = namesObj.names; // This does not.

More Information

The problems goes away if the NonReadonly type is defined as below.:

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 Writable<T> = Pick<T, WritableKeys<T>>;

type NonReadonly<T> = {
  [P in keyof Writable<T>]: T[P] extends number | string | boolean | undefined | null
    ? T[P]
    : NonReadonly<NonNullable<T[P]>>;
};

This was how it was defined in an earlier version of #813. See also: https://github.com/anymaniax/orval/pull/813#issuecomment-1502923146

Additional Question

Can the NonReadonly type be exported? This would come in handy when extending generated code.

What versions are you using?

Operating System: Win 10 Package Version: 6.14.3

localrobot avatar Apr 18 '23 08:04 localrobot

Hello @localrobot, for me in both case I have the problem

anymaniax avatar Apr 18 '23 09:04 anymaniax

This one seems to work properly no?

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]>;
};


type Name = { first: string; last: string };

type NamesObject = { names: Name[]; readonly id: string };

const namesObj: DeepWritable<NamesObject> = {
  names: [{ first: 'a', last: 'b' }],
};

const names: Name[] = namesObj.names; 

anymaniax avatar Apr 18 '23 09:04 anymaniax

I think we can also omit empty value type with this

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

type OmitEmptyKeys<T> = Omit<T, EmptyKeys<T>>;
type DeepWritableWithoutEmpty<T> = OmitEmptyKeys<DeepWritable<T>>;

all coming from here

anymaniax avatar Apr 18 '23 10:04 anymaniax

Those also seem to have problems:

type Name = { first: string; last: string, address?: { line1: string, line2?: string } };

type NamesObject = { names: Name[]; readonly id: string };

// Property 'address' is missing in type '{ first: string; last: string; }' but required in type
// 'DeepWritableObject<Name>'.ts(2741)
const namesObj: DeepWritable<NamesObject> = {
  names: [{ first: 'a', last: 'b' }],
};

const names: Name[] = namesObj.names; 

// Property 'line2' is missing in type '{ line1: string; }' but required in type 
// 'DeepWritableObject<{ line1: string; line2?: string | undefined; }>'.ts(2741)
const name: DeepWritable<Name> = { first: 'a', last: 'b', address: { line1: 'abc' } }

localrobot avatar Apr 19 '23 18:04 localrobot

So where are we with this ticket?

melloware avatar Nov 10 '23 03:11 melloware