-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Allow specifying only a subset of generic type parameters explicitly instead of all vs none #16597
Comments
We could see two things here:
@rbuckton and I think 1 makes a lot of sense even outside the context of this issue. |
I think specifying only some parameters should not be supported for two reasons:
In my opinion a better approach would be to implement an |
@gcnew I think this is a tooling problem, not a readability problem. You could also argue that not having a "diamond operator" is a readability problem. In Java, I do This came up multiple times when trying to restrict a parameter to a certain format. As a workaround, I curry the call, i.e. like Here's an example where I use this technique to implement a type-safe rename of attributes to make an API more palatable: class FormatDefinition<T> {
constructor(public handler: string) {}
get(resultName: keyof T): ReferenceInfo<T> {
return {
resultName,
handler: this.handler,
};
}
getFiltered(resultName: keyof T, filterSql: string): ReferenceInfo<T> {
const referenceInfo = this.get(resultName);
referenceInfo.filter = filterSql;
return referenceInfo;
}
}
/**
* Stores the rename mapping for type T.
*
* New name => old name
*
* Using keyof, we guarantee that the old name actually exists on T.
*/
interface Renamed<T> {
[index: string]: keyof T;
}
/**
* Use to provide user-readable names instead of the API names.
*
*/
class RenamedFormatDefinitionImpl<T, R extends Renamed<T>> extends FormatDefinition<T> {
constructor(handler: string, private renamed: R) {
super(handler);
}
/**
* Provide the name of a renamed field to get a format reference.
* @param resultName 22
*/
get(resultName: keyof R): ReferenceInfo<T> {
return super.get(this.renamed[resultName]);
}
}
/**
* Workaround: TypeScript doesn't allow specifying only one of two Type parameters.
* Parameter T is only used for the keyof validation. No object is passed in.
* Hence, it cannot be inferred automatically.
*
* Instead, split the constructor call into two calls, the first call with an explicit type parameter,
* the second is inferred.
*
* @param jspHandler JSP that provides the List functionality
*/
function RenamedFormatDefinition<T>(jspHandler: string) {
return function <R extends Renamed<T>>(renamed: R) {
return new RenamedFormatDefinitionImpl<T, R>(jspHandler, renamed);
};
}
interface ReferenceInfo<T> {
resultName: keyof T;
handler: string;
filter?: string;
}
interface FetchResult {
UGLY_NAME: string;
CODE: string;
KEY: string;
}
// desired syntax RenamedFormatDefinition<FetchResult>('api_endpoint', {CustomerId: 'UGLY_NAME'});
const ApiReference = RenamedFormatDefinition<FetchResult>('api_endpoint')({ CustomerId: 'UGLY_NAME' });
const key = 'CustomerId';
document.body.innerText = `${key} maps to ${ApiReference.get(key).resultName}`; Try it on Playground |
@alshain I see where you are coming from and I definitely sympathise with the desire to create safer APIs. In the above example, the type parameter |
One thing I liked in C# was the Action and Func generics. In typescript if I try to define them I get an error, "Duplicate identifier Action"
So, if you did an optionality marker, would that mean I could just do this? type Action<T1?, T2?, T3?, T4?> = (v1?: T1, v2?: T2, v3?: T3, v4?: T4) => void Although that's good it's maybe not ideal since if I make an Action I want it to have one required argument, not 4 optional arguments. |
Of course yes, that's why I would like to be able to provide only a subset of generic parameters explicitly, exactly because phantom types cannot be inferred.
This is a good thought, thanks! In this case, I'm exporting But even with the overload approach, I, as the module writer, would need to provide both type parameters explicitly---or use currying. I still think TypeScript should support specifying only a subset of the type parameters. This is not only useful for phantom types, it's also useful for types with multiple generic parameters where not all can be deduced from the constructor, because some parameters only occur in methods/fields. |
Hi, Has there been any further decision on this? function test<S, T extends keyof S>(tValue: T): any {
return null;
}
// Want to do
test<SomeType>('some-key');
// Have to do
test<SomeType, 'some-key'>('some-key'); |
For anyone else wondering about a workaround, as @alshain said you can use currying. The workaround for the above example would be: function test<S>() {
return function<T extends keyof S>(tValue: T): any {
return null;
};
}
test<SomeType>()('some-key'); |
I too am currently using currying as well as workaround, but it feels too hacky to me. I wish this can be resolved. |
We could use the |
+1 to this issue, would love to see it resolved! |
The currying workaround may not be an option if this is for a type predicate, e.g. we can't rewrite this to be curried because the type DistributedKeyOf<T> = T extends Unrestricted ? keyof T : never;
type Require<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
const has = <T extends object, K extends DistributedKeyOf<T>>(
source: T,
property: K,
): source is T & Require<Pick<T, K>, K> => property in source; |
ngrx does an interesting hack to solve this for now: see: export declare function createAction<T extends string>(type: T): ActionCreator<T, () => TypedAction<T>>;
export declare function createAction<T extends string, P extends object>(type: T, config: {
_as: 'props';
_p: P;
}): ActionCreator<T, (props: P) => P & TypedAction<T>>;
export declare function props<P extends object>(): PropsReturnType<P>;
export declare type PropsReturnType<T extends object> = T extends {
type: any;
} ? TypePropertyIsNotAllowed : {
_as: 'props';
_p: T;
}; So you can optional only set the second generic value with the In use it looks like this: const action = createAction('foobar', props<{lorem: string}>());
// ^^^^^^^^^^^^^^^^^^^^^^^^ using props method to set second generic
action({lorem: 'test'});
// ^^^^^^^^^^^^^ <- action knows the props generic, as it was caried over And |
I'm not sure if this point could fit into this issue or not, but it would be nice for TS to support the diamond operator like Java has (cfr https://www.baeldung.com/java-diamond-operator). With it, the following declaration:
Could become:
Should I create a separate ticket for that feature request? |
@dsebastien you can just do |
it's super annoying having to do this, |
There is another use for this, where people are asking for It would be beneficial to be able to say: // foo is `FooType & typeof y`
const foo = Object.assign<FooType>(x, y); Of course the type system can already do this, because we can say: type P = {p:number};
type Q = {q:number};
type PQx={
p:number;
q?:number;
};
function TypedAssigner<T>(){
return function<U>(t:T, u:U){
return Object.assign(t, u);
};
}
const foo = TypedAssigner<PQx>()({p:1}, {q:2}); This would also enable excess property checks in the case where There are certainly other uses, but surely there is a case to allow specifying the first type parameter and inferring the remaining type parameters. |
The fact that this is doable with currying means that the inference is clearly feasible, particularly if it's an opt-in. But currying just results in too-awkward of an API in many cases, and is a bit of a dealbreaker when designing usable APIs. One recent use case that came up was verifying the type inferred by various expressions. I'd like to be able to write (assuming #23689 is resolved): declare function assertExactType<T, U? extends T>(arg: U): T extends U ? void : invalid<`Expected ${T} but got ${U}`>; Alternatively, declare const foo: Foo;
declare const bar: Bar;
assertExactType<Baz<Bar>>(foo.method(bar)); If this typechecks, then we have confidence that inference is behaving exactly as expected. My current workaround is a lot more fidgety due to the currying and random extra |
Thinking a little more about this, it might also be useful to prevent specifying the inferred parameters explicitly. A syntax along the lines of function satisfies<T, infer U extends T>(arg: U): U { return arg; } could make it look less like just providing an overridable default and more like an indication that you specifically want the type checker to fill it in. |
TypeScript Version: 2.3.0
https://www.typescriptlang.org/play/
Code
Expected behavior:
The compiler should infer
S
to bestring
.Actual behavior:
Error:
The compiler expects either all generic parameters to be specified explicitly or none at all.
The text was updated successfully, but these errors were encountered: