Typescript: Allow specifying only a subset of generic type parameters explicitly instead of all vs none

Created on 17 Jun 2017  路  16Comments  路  Source: microsoft/TypeScript



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.

In Discussion Suggestion

Most helpful comment

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

All 16 comments

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.

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.

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

@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.

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

@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.

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

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 infer keyword to suggest TS that we don't want to touch that parameter.

+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 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;

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

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?

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DanielRosenwasser picture DanielRosenwasser  路  3Comments

blendsdk picture blendsdk  路  3Comments

remojansen picture remojansen  路  3Comments

dlaberge picture dlaberge  路  3Comments

wmaurer picture wmaurer  路  3Comments