Narrowing a return type from a generic, discriminated union in TypeScript

前端 未结 5 2065
执念已碎
执念已碎 2020-12-09 03:09

I have a class method which accepts a single argument as a string and returns an object which has the matching type property. This method is used to narrow a di

5条回答
  •  死守一世寂寞
    2020-12-09 03:44

    Like many good solutions in programming, you achieve this by adding a layer of indirection.

    Specifically, what we can do here is add a table between action tags (i.e. "Example" and "Another") and their respective payloads.

    type ActionPayloadTable = {
        "Example": { example: true },
        "Another": { another: true },
    }
    

    then what we can do is create a helper type that tags each payload with a specific property that maps to each action tag:

    type TagWithKey = {
        [K in keyof T]: { [_ in TagName]: K } & T[K]
    };
    

    Which we'll use to create a table between the action types and the full action objects themselves:

    type ActionTable = TagWithKey<"type", ActionPayloadTable>;
    

    This was an easier (albeit way less clear) way of writing:

    type ActionTable = {
        "Example": { type: "Example" } & { example: true },
        "Another": { type: "Another" } & { another: true },
    }
    

    Now we can create convenient names for each of out actions:

    type ExampleAction = ActionTable["Example"];
    type AnotherAction = ActionTable["Another"];
    

    And we can either create a union by writing

    type MyActions = ExampleAction | AnotherAction;
    

    or we can spare ourselves from updating the union each time we add a new action by writing

    type Unionize = T[keyof T];
    
    type MyActions = Unionize;
    

    Finally we can move on to the class you had. Instead of parameterizing on the actions, we'll parameterize on an action table instead.

    declare class Example {
      doSomething(key: ActionName): Table[ActionName];
    }
    
    
    

    That's probably the part that will make the most sense - Example basically just maps the inputs of your table to its outputs.

    In all, here's the code.

    /**
     * Adds a property of a certain name and maps it to each property's key.
     * For example,
     *
     *   ```
     *   type ActionPayloadTable = {
     *     "Hello": { foo: true },
     *     "World": { bar: true },
     *   }
     *  
     *   type Foo = TagWithKey<"greeting", ActionPayloadTable>; 
     *   ```
     *
     * is more or less equivalent to
     *
     *   ```
     *   type Foo = {
     *     "Hello": { greeting: "Hello", foo: true },
     *     "World": { greeting: "World", bar: true },
     *   }
     *   ```
     */
    type TagWithKey = {
        [K in keyof T]: { [_ in TagName]: K } & T[K]
    };
    
    type Unionize = T[keyof T];
    
    type ActionPayloadTable = {
        "Example": { example: true },
        "Another": { another: true },
    }
    
    type ActionTable = TagWithKey<"type", ActionPayloadTable>;
    
    type ExampleAction = ActionTable["Example"];
    type AnotherAction = ActionTable["Another"];
    
    type MyActions = Unionize
    
    declare class Example
    { doSomething(key: ActionName): Table[ActionName]; } const items = new Example(); const result1 = items.doSomething("Example"); console.log(result1.example);

    提交回复
    热议问题