简体   繁体   中英

Nested Generics in function definition in TypeScript

I'm trying to make type-safe handlers for redux actions relying on action type. For example, any action could be described as:

type ActionType<T extends string> = {
  type: T
};

For a specific action one can have:

type MyAction = ActionType<'hey' | 'there'>;

Now, I'd like to restrict a handler function to allow only 'hey' or 'there' as a type. Finally I expect something like this:

handleAction<MyAction>('hey');

where the definition of the handler function could be:

function handleAction<A extends ActionType<T>>(type: T){
...
}

But I have a typescript compiler error:


TS2304: Cannot find name 'T'.


So, I have to modify this handler function definition in this way:

function handleAction<A extends ActionType<T>, T extends string>(type: T){
...
}

It works, but looks really ugly:

handleAction<MyAction, 'hey' | 'there'>('hey');

TS Playground

What's a better way to handle this?

Edit

You can use a type query to get the type of type :

function handleAction<A extends ActionType<string>>(type: A ['type']) {
}

handleAction<MyAction>('hey');

Or you can use conditional types in 2.8 to extract the generic parameter from ActionType (2.8 is unreleased at the time of writing, but will be released in March 2018, you can get it via npm install -g typescript@next )

type ActionType<T extends string> = {
  type: T
};

type MyAction = ActionType<'hey' | 'there'>;


function handleAction<A extends ActionType<string>>(type: A extends ActionType<infer U> ? U : never) {
}

handleAction<MyAction>('hey');

This solution is based on the inference behavior of conditional type . ActionType<infer U> basically says: If a extends ActionType of some other type U give me the type U . So U will be whatever string literal type was passed to ActionType in our case. Then we use U on the true branch of the conditional type and this becomes the final type of out conditional type. In this case we don't care about the false branch so we use never.

There are two approaches:

  1. Use the string union.

I like this one because its simpler (for the problem you described):

type FileAction = 'save' | 'read' | 'copy';
function doAction (a) {
}
declare function doAction<A extends string> (action: A): void;

doAction<FileAction>('save'); // good, as desired
doAction<FileAction>('xxx'); // error, as expected
  1. Get .type via bracket notation.

This is one looks more hacky but it's a valid syntax .. and it does not require you to change your object shape.

type FileFile <T extends string> = {
  type: T;
}
type WordFile = FileFile<'why'|'you'|'no'|'work'>;

function doAction2 (a) {
}
declare function doAction2<F extends FileFile<string>> (action: F["type"]): void;

doAction2<WordFile>('no'); // good, as desired 
doAction2<WordFile>('xxx'); // error, as expected

This cleans things up some, but I'm not sure if it's what you're hoping for:

type ActionType<T extends string> = {
  type: T
};

type MyWord = 'hey' | 'there';
type MyAction = ActionType<MyWord>;

function handleAction<A extends ActionType<T>, T extends string>(type: T){
}

handleAction<MyAction, MyWord>('hey');

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM