Skip to content

Readonly GenericsΒ #45311

Closed
Closed
@SephReed

Description

@SephReed

Suggestion

πŸ” Search Terms

Generics, readonly

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

The ability to define contextual, readonly generics for cleaner code and context-specific generics.

πŸ“ƒ Motivating Example

someFunction<T extends object, readonly KEYS = Array<Extract<keyof T, string>>>(obj: T): {
  propsOfNumberType: KEYS;
  propsOfStringType: KEYS;
  propsOfBooleanType: KEYS;
  propsOfObjectType: KEYS;
}{
 //... implementation omitted
}

πŸ’» Use Cases

The use case for this is any time you have a complex generic which is used multiple times, but should never be specified by the user.

The major benefits of this are:

  1. cleaner code - By being able to give a readonly generic a shorter name, you can avoid duplicating its code without obfuscating the type.. In the example above, you could create a type KEYS<T> = Array[keyof T]>, but most utility generic types have much longer names such as: RequiredKeys, SubpropertyMerge, NullableKeys, UnionToIntersection

  2. more performant - For especially heavy generics (usually involving infer, or recursion), being able to define them once and then re-use their definition would be much more performant than compiling the type for every use.

  3. class wide types - A very useful typescript feature is that in functions you can do:

     function <T>(t: T) {
       type S = SomeComplex<T> & MutationOf<T>
       const eg: { foo: S;  bar: S;  qux: S } = ....
     }

    unfortunately, this is not possible in classes, and so you must spell out the same generics over and over again if they are meant to be inferred from other generics


A more elaborate example

export abstract class QueryTools<
	T extends object,
	ARGS extends Partial<T>,
        readonly KEYS =  KeyOf<T>,
        readonly KEY_ARR =  Array<KEYS>,
        readonly PART = Partial<T>
> {
	public abstract query(): Knex.QueryBuilder<T>;

	public abstract get<SEL extends KEY_ARR>(
		id: string,
		select?: SEL
	): Promise<null | QueryResponse<T, SEL>>

	public abstract getByFn<
		KEY extends KEYS
	>(key: KEY): <SEL extends KEY_ARR>(value: T[KEY], select?: SEL) => Promise<null | QueryResponse<T, SEL>>;

	public abstract require<SEL extends KEY_ARR>(
		id: string,
		select?: SEL
	): Promise<QueryResponse<T, SEL>>;

	public abstract requireByFn<
		KEY extends KEYS
	>(key: KEY): <SEL extends KEY_ARR>(value: T[KEY], select?: SEL) => Promise<QueryResponse<T, SEL>>;

	public abstract requireOne<SEL extends KEY_ARR>(
		where: PART,
		select?: SEL
	): Promise<QueryResponse<T, SEL>>;

	public abstract hydrate<
		SEL extends KEY_ARR,
		OBJ extends PART
	>(
		obj: OBJ,
		hydrations: SEL,
		args?: {
			refresh?: boolean;
		}
	): Promise<QueryResponse<T, [Extract<RequiredKeys<OBJ>, KEYS> | SEL[number]]>>;

	public abstract updateWithObject(
		obj: PART,
		updates: PART
	): Promise<PART>;

	public abstract syncObject(obj: PART): Promise<PART>;

	public abstract update(
		where: PART,
		updates: PART,
		args?: { limit: number; }
	): Promise<number>;

	public abstract updateOne(
		where: PART,
		updates: PART
	): Promise<number>;

	public abstract updateById(
		id: string,
		updates: PART
	): Promise<number>;

	public abstract updateByFn<
		KEY extends KEYS
	>(key: KEY): (keyValue: T[KEY], updates: PART) => Promise<number>;

	public abstract findOne<SEL extends KEY_ARR>(
		where: PART,
		select?: SEL
	): Promise<null | (QueryResponse<T, SEL>)>

	public abstract find<SEL extends KEY_ARR>(
		where: PART,
		args?: {
			limit?: number,
			select?: SEL,
			// sort?: {[key in KeyOf<T>]?: number}
		}
	): Promise<Array<QueryResponse<T, SEL>>>;

	public abstract deleteById(id: string, confirm: "BLAME"): Promise<number>;

	public abstract create<BASE extends ARGS>(createMe: BASE): Promise<T & BASE>;
}

Having readonly generics cleans up this code a tremendous amount. If you'd like to see it without any readonly generics, here's a playground link

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions