microsoft/TypeScript

allow type parameters on type arguments

zpdDG4gta8XKpMCd opened this issue · 6 comments

interface Map<a> {
   [key: string]: a;
}

// IMPORTANT: here my intention is that the `read` and `run` methods are agreed on using the same type parameter
interface Command<Options> {
   read(args: string[]): Options;
   run(options: Options): void;
};

interface CopyCommandOptions {
   from: string;
   to:  string;
}

interface CleanCommandOptions {
   directory: string;
   pattern: string;
}


// currently in TypeScript there are 2 options to to declare `knownCommands` below:
// - Map<Command<any>> which contaminates my code with `any`
// - Map<Command<CopyCommandOptions | CleanCommandOptions>> which has different semantics
// ideally I wish I could declare it like this:
let knownCommands : Map<<a>Command<a>> = { // <-- hypothetical syntax
    'copy': <Command<CopyCommandOptions>> undefined,
    'clean': <Command<CleanCommandOptions>> undefined
};

// so that later I could the following do in a type safe manner:
let command = knownCommands[name];
command.run(command.read(args));

Note for others that this is not the same as #1213. The idea on this issue is that there is a constraint on the generic type, but that the type arguments themselves should be inferred.

This is similar to the desired behavior in #5652.

From reading your example, it looks to me like you're wanting existential types?

Keep in mind that I haven't really done any typescript before - so there's likely a nicer way to do what I came up with:

type Exists =
    <r> (go : <t> (_ : Command<t>) => r) => r

const exists = <c> (command : Command<c>) : Exists =>
    <r> (go : <t> (_ : Command<t>) => r) : r => go(command)

let knownCommands : Map<Exists> = {
    'copy': exists(<Command<CopyCommandOptions>> undefined),
    'clean': exists(<Command<CleanCommandOptions>> undefined)
};

let args : string[]
knownCommands[name](<t> (command : Command<t>) => {
    command.run(command.read(args));
});

Does that roughly do what you want in this case? @Aleksey-Bykov

@LiamGoodacre
This is a neat trick. Didn't realize I could do it this way. Yes, it looks like existential types is the name of what I've been looking for, which now can be googled and read about closely. Thanks.

😄 Also to highlight that the read/run types match up:

knownCommands[a](<t> (x : Command<t>) => {
    knownCommands[b]<s> (y : Command<s>) => {
        //x.run(y.read(args)); // type error, `s` is not `t`
        //y.run(x.read(args)); // type error, `t` is not `s`
        x.run(x.read(args)); // fine
        y.run(y.read(args)); // fine
    });
});

This may be useful as an example of existential types as a library: https://github.com/purescript/purescript-exists/blob/master/docs/Data/Exists.md

This hasn't gotten a concrete proposal yet and I imagine there are more well-fleshed-out issues on it by now