Description
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:
-
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 -
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.
-
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