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

Allow specifying only a subset of generic type parameters explicitly instead of all vs none #16597

Open
alshain opened this issue Jun 17, 2017 · 19 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@alshain
Copy link

alshain commented Jun 17, 2017

TypeScript Version: 2.3.0
https://www.typescriptlang.org/play/
Code

class Greeter<T, S> {
    greeting: T;
    constructor(message: T, message2: S) {
        this.greeting = message;
    }

}

let greeter = new Greeter<string>("Hello", "world");

Expected behavior:

The compiler should infer S to be string.

Actual behavior:
Error:

Supplied parameters do not match any signature of call target.

The compiler expects either all generic parameters to be specified explicitly or none at all.

@alshain alshain changed the title Allow specifying only a subset of type parameters explicitly instead of all vs none Allow specifying only a subset of generic type parameters explicitly instead of all vs none Jun 17, 2017
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jun 19, 2017

We could see two things here:

  1. Currently you can have a generic default like = {}, however, if any type arguments are provided explicitly, TypeScript always takes the default itself instead of trying to make inferences, but we could change this.
  2. We could allow type parameters to have an optionality marker (?) like regular parameters.

@rbuckton and I think 1 makes a lot of sense even outside the context of this issue.

@DanielRosenwasser DanielRosenwasser added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jun 19, 2017
@gcnew
Copy link
Contributor

gcnew commented Jun 19, 2017

I think specifying only some parameters should not be supported for two reasons:

  • generic inference is quite hard as is
  • it will not be obvious for readers how many parameters are needed, which are provided and which are inferred, etc

In my opinion a better approach would be to implement an is operator (can't find the link where it was suggested, its a safer alternative of as). This way the programmer would be able to hint the type of a specific parameter in a safe manner. On the up side, no alternations would be needed to the inference algorithm.

@alshain
Copy link
Author

alshain commented Jun 20, 2017

it will not be obvious for readers how many parameters are needed, which are provided and which are inferred, etc

@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 new ArrayList<>() and it's evident that the parameters are inferred. In TypeScript, I would do new ArrayList(), and generics are no where to be seen whatsoever. Except on hover in VS Code and the likes, of course.

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
function <ExplicitParam>(e) { return function<InferredParam>(i) {} }

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

@gcnew
Copy link
Contributor

gcnew commented Jun 20, 2017

@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 T is phantom - there are no actual parameters values with that type, thus it can never be inferred. From an API standpoint I don't think that's ideal, as it puts the burden of specifying a presumably known T (assuming each api_endpoint maps to a specific predefined T) on the consumer. Wouldn't adding several overloads for the several expected FetchResults be a better approach? As it is now, the consumer is the one who decides what the resulting type of api_endpoint is - an implicit knowledge that is not actually type checked or enforced.

@mblandfo
Copy link

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"

type Action = () => void;
type Action<T> = (v: T) => void;

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.

@alshain
Copy link
Author

alshain commented Jun 22, 2017

@gcnew

In the above example, the type parameter T is phantom - there are no actual parameters values with that type, thus it can never be inferred.

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.

From an API standpoint I don't think that's ideal, as it puts the burden of specifying a presumably known T (assuming each api_endpoint maps to a specific predefined T) on the consumer. Wouldn't adding several overloads for the several expected FetchResults be a better approach? As it is now, the consumer is the one who decides what the resulting type of api_endpoint is - an implicit knowledge that is not actually type checked or enforced.

This is a good thought, thanks! In this case, I'm exporting ApiReference etc, so the consumer doesn't need to provide the name of the API endpoint by themselves. Actually, I'm both the consumer and the library author in this case anyway :) With the exports, the consumer can't decide what the resulting type is anymore.

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.

@alisd23
Copy link

alisd23 commented Mar 11, 2018

Hi,

Has there been any further decision on this?
I'm currently dealing with a complex typing situation where this would be helpful, in my case where the second generic parameter extends keyof the first:

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');

@alisd23
Copy link

alisd23 commented Mar 11, 2018

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');

@joonhocho
Copy link

I too am currently using currying as well as workaround, but it feels too hacky to me. I wish this can be resolved.

@Akxe
Copy link

Akxe commented Jul 26, 2019

We could use the infer keyword to suggest TS that we don't want to touch that parameter.

@agstrauss
Copy link

+1 to this issue, would love to see it resolved!

@OliverJAsh
Copy link
Contributor

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 source parameter must be a direct parameter of the user-defined type guard:

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;

@sod
Copy link

sod commented Nov 12, 2019

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 props functions, while the first generic stil being inferred.

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 action.type (the first generic) is inferred from the first argument.

image

@dsebastien
Copy link

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:

fooByReference: Map<string, Foo> = new Map<string, Foo>();

Could become:

fooByReference: Map<string, Foo> = new Map<>();

Should I create a separate ticket for that feature request?

@Quelklef
Copy link

Quelklef commented Jun 28, 2020

@dsebastien you can just do map: Map<K, R> = new Map();; see playground

@thealjey-eib
Copy link

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');

it's super annoying having to do this,
but it's the only approach that actually works

@bliddicott-scottlogic
Copy link

bliddicott-scottlogic commented Dec 16, 2021

There is another use for this, where people are asking for Object.assign which reports excess properties. E.g. #47130

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 y was an object literal, as referenced in #47130.

There are certainly other uses, but surely there is a case to allow specifying the first type parameter and inferring the remaining type parameters.

@shicks
Copy link
Contributor

shicks commented Aug 25, 2022

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, <T, U extends T = infer> could also be a reasonable way to opt in. This would allow writing very clear and concise type checking tests:

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 ().

@shicks
Copy link
Contributor

shicks commented Aug 29, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests