2017-08-02 76 views
11

我已经Action类型定义是这样的:类型检查createAction终极版帮手

type Action = 
    { type: 'DO_X' } 
    | 
    { type: 'DO_Y', payload: string } 
    | 
    { type: 'DO_Z', payload: number } 

这是一个联合类型,其中每个成员都是有效的行动。

现在我想创建一个函数createAction,它接受type并返回一个接受​​的新函数。

const doZ = createAction('DO_Z') 
console.log(doZ(42)) // { type: 'DO_Z', payload: 42 } 

这里是我当前的实现:

const createAction = (type: Action['type']) => 
    (payload?: any) => 
    ({ type, payload }) 

它typechecks type像我想。我怎样才能也typecheck​​?我想要​​根据type匹配正确操作的类型。例如,doZ在调用string时应该失败,因为它的​​表示它只接受number

+1

您是否尝试过类似于(type:Type)的定义:(payload:Payload)=> {type:Type,payload:Payload}? –

+0

@EmrysMyrooin这似乎并不奏效。类型被用作值。但仿制药似乎是朝着正确方向迈出的一步。 TypeScript如何知道'Type'和'Payload'来自'Action'? – daGrevis

+0

何你在Typescript ...我没有检查过标签。我实际上不知道,我正在使用Flow进行打字! –

回答

4

这个问题的规范答案取决于你的确切用例。我假设你需要Action来准确评估你写的类型;也就是type: "DO_X"的对象不是有​​任何形式的财产。这意味着createAction("DO_X")应该是零参数的函数,而createAction("DO_Y")应该是单个string参数的函数。我还将假设您想要自动推断createAction()上的任何类型参数,因此您不需要为Blah的任何值指定createAction<Blah>("DO_Z")。如果解除这些限制中的任何一个,则可以将解决方案简化为类似于@Arnavion提供的解决方案。


打字稿不喜欢物业映射类型,但它高兴地物业这么做。因此,让我们以一种为编译器可用来帮助我们的类型提供类型的方式构建Action类型。首先,我们描述了有效载荷的每一个动作类型是这样的:

type ActionPayloads = { 
    DO_Y: string; 
    DO_Z: number; 
} 

我们还引入任何Action类型,而有效载荷:

type PayloadlessActionTypes = "DO_X" | "DO_W"; 

(我已经添加了'DO_W'型只是为了显示它是如何工作,但你可以删除它)。

现在,我们终于能够表达Action

type ActionMap = {[K in keyof ActionPayloads]: { type: K; payload: ActionPayloads[K] }} & {[K in PayloadlessActionTypes]: { type: K }}; 
type Action = ActionMap[keyof ActionMap]; 

ActionMap类型是一个对象,它的键是每个Actiontype,并且其值是Action工会的相应元素。它是Action s与​​s的交点,以及Action而不是​​s的交点。而Action只是ActionMap的值类型。确认Action是您所期望的。

我们可以用ActionMap帮我们输入createAction()函数。那就是:

function createAction<T extends PayloadlessActionTypes>(type: T):() => ActionMap[T]; 
function createAction<T extends keyof ActionPayloads>(type: T): (payload: ActionPayloads[T]) => ActionMap[T]; 
function createAction(type: string) { 
    return (payload?: any) => (typeof payload === 'undefined' ? { type } : { type, payload }); 
} 

这是一个重载函数与对应的Actiontype要创建一个类型参数T。前两个声明描述了两种情况:如果TtypeAction而没有​​,则返回类型是返回正确类型Action的零参数函数。否则,它是一个单参数函数,它采用正确类型的​​并返回正确类型Action。实施(第三签名和身体)与你相似,不同之处在于,如果在没有​​通过它不添加​​的结果。


全部完成!我们可以看到,它的工作原理是期望:

var x = createAction("DO_X")(); // x: { type: "DO_X"; } 
var y = createAction("DO_Y")("foo"); // y: { type: "DO_Y"; payload: string; } 
var z = createAction("DO_Z")(5); // z: { type: "DO_Z"; payload: number; } 

createAction("DO_X")('foo'); // too many arguments 
createAction("DO_X")(undefined); // still too many arguments 
createAction("DO_Y")(5); // 5 is not a string 
createAction("DO_Z")(); // too few arguments 
createAction("DO_Z")(5, 5); // too many arguments 

你可以看到这一切在行动on the TypeScript Playground。希望对你有效。祝你好运!

1

冗长,但这个工程:

type XAction = { type: 'DO_X', payload: undefined }; 
type YAction = { type: 'DO_Y', payload: string }; 
type ZAction = { type: 'DO_Z', payload: number }; 

type Action = XAction | YAction | ZAction; 

const createAction = <T extends Action>(type: T['type']) => 
    (payload: T['payload']) => 
     ({ type, payload }); 

// Do compile: 

createAction<XAction>("DO_X")(undefined); 
createAction<YAction>("DO_Y")("foo"); 
createAction<ZAction>("DO_Z")(5); 

// Don't compile: 

createAction<XAction>("DO_X")(5); // Expected `undefined`, got number 
createAction<YAction>("DO_Y")(5); // Expected string, got number 
createAction<ZAction>("DO_X")(5); // Expected `"DO_Z"`, got `"DO_X"` 

更简单的方法(不是强迫的createAction类型参数):

type Action = { type: 'DO_X', payload: undefined } | { type: 'DO_Y', payload: string } | { type: 'DO_Z', payload: number }; 

createAction("DO_Y")("foo"); 

不幸允许createAction<YAction>("DO_Y")(5)等被编译,因为T总是推断为Action,因此​​参数是string|number|undefined