Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Readonly Generics #45311

Closed
5 tasks done
SephReed opened this issue Aug 3, 2021 · 10 comments
Closed
5 tasks done

Readonly Generics #45311

SephReed opened this issue Aug 3, 2021 · 10 comments
Labels
Duplicate An existing issue was already created

Comments

@SephReed
Copy link

SephReed commented Aug 3, 2021

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

@jcalz
Copy link
Contributor

jcalz commented Aug 3, 2021

Other than syntax, is this different from #7061?

@SephReed
Copy link
Author

SephReed commented Aug 3, 2021

A bit different. This also works for interfaces, or types. Eg:

interface Test<T, readonly KEYS = keyof T> {
  a: KEYS;
  b: KEYS;
}

type Test<T, readonly KEYS = keyof T> = { [KEY in KEYS]: number } | { a: KEYS };

Also, it seems a fair bit easier to implement given that removing the "readonly" is already valid code, and adding it only requires suppressing a few errors.

@MartinJohns
Copy link
Contributor

Sounds like you want #9889.

@SephReed
Copy link
Author

SephReed commented Aug 3, 2021

@MartinJohns, close but my use case is actually a type:

type SomeFn = <T, readonly KEYS = keyof T>() => { a: KEYS, b: KEYS, c: KEYS }

And, personally, I have had to convert many of my interfaces to types before. Eg:

interface Arg = { a : 1 }

.... some time later

type Arg = { a: 1 } | { b: 2}

@MartinJohns
Copy link
Contributor

Okay, then it sounds like you want #41470.

The keyword readonly really makes no sense to me here, because there's nothing writable in the first place. Type arguments are provided (or additionally have a default). The syntax you propose seems really confusing, because I would expect to use it as SomeFn<TypeA, TypeB>, while you seemingly think of it being used as SomeFn<TypeA>.

@SephReed
Copy link
Author

SephReed commented Aug 3, 2021

@MartinJohns making three different syntaxes for caching a generic seems like a bad suggestion, IMO. Classes, interfaces, and types can share the same syntax.

Also, totally open to suggestions on readonly syntax. It doesn't really matter what the word is to me.

But -- as a UX expert -- I would strongly suggest keeping the placement. Generics evolve as the problem space does, so being able to easily switch between KEYS extends keyof T = keyof T and yourSuggestionForReadonly KEYS = keyof T would very likely give a better user experience than moving it around.

Also, it could be used the same across the board, and it would not inadvertently create a separation of interest.

@SephReed
Copy link
Author

SephReed commented Aug 3, 2021

That being said, if you get some good UX devs into a room together, they could find something better. Assumably, it would also have the same syntax for classes, interfaces, functions, and types.

@RyanCavanaugh
Copy link
Member

What you're describing isn't a type parameter, at all. It has nothing in common with how type parameters work. For example, given how you've defined them, you would expect to be able to use a keyof T to satisfy a KEYS, but that isn't how type parameters are defined since might be instantiated with a more-specific type.

This is #41470 or #23188, because putting a type alias (which is what you're talking about here) in a type parameter list is a) misleading and b) dominated by letting you put it anywhere in a type body, since you might have all kinds of local type expressions you want to capture.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Aug 4, 2021
@SephReed
Copy link
Author

SephReed commented Aug 6, 2021

It has nothing in common with how type parameters work

As a user facing dev, I think you should be better at putting yourself in user shoes.

The majority of devs use generics in a way that they are inferred from usage, and largely invisible to users. They are generally just a means of passing some user supplied typing back to them.

Also, neither of the issues you've listed would cut it for what I was looking for.


On a side note: Is there anyone above Ryan I can complain to? At this point I've seen 30+ different great ideas for typescript that he's been a complete jerk about, some with many hundreds of supporters. And I've never once seen him even acknowledge that he's not the only person who's feelings matter.

@SephReed
Copy link
Author

SephReed commented Aug 6, 2021

Seriously though. If you're someone who gets paid a very decent living to be a user facing dev, you should at least understand that basics of UX. And one of the biggest basics of UX is that no matter how important you might think you are, you only count as one user.. It's introductory as far as research and the scientific process are involved.

But there's no sense of science or UX here. Time and time again I've seen Ryan respond in subjective threads as though his view on the subjective matter outweighs all else; finding one single aspect he doesn't like and -- rather than trying to ideate an improvement or clarify its meaning -- calling it grounds for dismissal.

Most amazing developers aren't great at being user facing -- nor do they enjoy to be -- so I think it might benefit everyone for someone to recognize what's going on here. And if Ryan really does enjoy shooting things down with zero effort to ideate or understand, that's even worse.

It's worth being said.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants