8

我正在尝试将我的 angular 2 应用程序迁移到 angular 4 和 ngrx 4。

我正在处理一个奇怪的打字稿编译问题,这是我在此更新之前没有遇到的。我在离线环境中工作,所以我不能在这里分享确切的代码。

我在网上寻找问题,我能找到的最接近的问题是这个问题: 关于有效负载类型 的问题这个问题中的代码在某种程度上类似于我试图做的事情,但不同的是我从动作中删除了 ActionTypes 对象(这是该问题的建议答案)但它没有解决我的编译错误。

我不能把我要处理的确切代码放在这里,但我的代码完全基于 github 上的 ngrx 示例 ngrx 官方示例

让我们看一下 book reducer 和 book actions book actions book reducer的例子

我的代码基本相同,当我尝试从操作中获取有效负载时,每个减速器案例(在 switch 案例中)都会发生错误,它告诉我如下信息:

Type 'string | Book | Book[]' is not assignable to type 'Book'. Type 'string' is not assignable to type 'Book'.

(在我的代码中,我有不同的类,而不是 Book,但它的想法几乎相同)

现在我通过从 A | 转换动作类型来解决错误。乙| B 为 switch 中 case 对应的具体动作类型(使用 as typescript 关键字)。这个解决方案感觉很糟糕和hacky,我想知道是否有更好的解决方案。

我使用的版本:打字稿:2.4.1(我怀疑它可能连接到不使用与我相同的更新打字稿版本的ngrx示例)ngrx@store:4.0.3 ngrx@core:1.2.0 angular:(最新为17.9.17,有很多包里面有角度,我认为这个错误使用哪个角度并不重要,但我说这只是为了安全起见)打字稿:(2.4.1)(我读了ngrx关于需要升级打字稿的官方迁移文档,所以我这样做了)


更新:由于我得到了一个与我的问题无关的答案并且我被问到,这是我的代码失败的部分(我无法发布确切的帖子,但我手动复制了相关部分):

任务减速器.ts:

import * as MissionActionsFile from "./mission-actions";

export type State = .... // doesnt matter

const initialState: State = // doesnt matter as well

export function reducer(state: State = initialState, action: 
    MissionActionsFile.Actions): State {

    switch(action.type) {

        case MissionActionsFile.ADD_SUCCESS: 
        {
            const mission: Mission = action.payload; // This line generates the 
            // following error: 
            // Type 'string | number | Mission | MissionRealTime | ...' is not 
            // assignable 
            // to type 'Mission'. Type 'string' is not assignable to type 'Mission'

            return {}; // This doesnt matter too
        }

        // There are more cases and a deafult one, but that doesn't matter
    }
}

任务-actions.ts:

// I will only give the actual action defenition for AddSuccess and for AddFailed since we are using AddSuccess in the example and 
// addFailed is an example where the payload type is string, which according to the error is the problem, though
// there are more action types and other payload types with other classes as the quoted error implies.

export const ADD_SUCCESS: string = "[Mission] AddSuccess";
export const ADD_FAILED: string = "[Mission] AddFailed";

export class AddSuccessAction implements Action {
    public readonly type: string = ADD_SUCCESS;

    constructor(public payload: Mission) {}
}

export class AddFailedAction implements Action {
    public readonly type:string = ADD_FAILED;

    constructor(public payload: string) {}
}

// More actions are defined here but im not gonna copy all

...

export type Actions =
    AddSuccessAction |
    AddFailedAction;

所以总而言之,我确实明白为什么 typescript 认为动作有效负载类型可能是 string | 是有意义的。使命 | ...但是在我基于此的ngrx示例中,似乎打字稿知道在那里推断案例中的特定类型,但对我来说,由于我不理解的原因这不起作用,可能与我有关使用打字稿 2.4.1?不确定,需要帮助

4

2 回答 2

13

问题是代码的行为和它的类型注释是相互交叉的。

实际上,我会说代码被过度注释。

联合类型和它们通过类型推断和基于控制流的类型分析实现的案例分析,这是 TypeScript 最强大的两个功能。语言从联合中消除可能性的过程称为缩小

正如代码所示,可以通过对称为判别式的属性执行值测试来将联合类型分解为其组成部分。

判别式是一种属性,其类型具有一组有限的可能值,每个值通常对应于并集的一个情况。

该类型string不是有效的判别式,但该类型"hello world"是因为,作为所有可能字符串的超类型,包括string折叠在内的字符串类型的并集只是string. 例如, typestring | "hello world"正是 type string

当我们在 TypeScript 中定义一个const或一个readonly属性时,编译器将其类型推断为初始化文字的类型。

考虑:

const kind = "first";

这里的类型kind不是string但是"first"

同样,给定

class Kindred {
  readonly kind = "first";
}

属性的类型kind不是stringbut "first"

在我们的代码中,判别式是一个名为的属性type,由联合的每个组成部分定义。

但是,虽然您已经正确地为每个成员提供了唯一值,但您使用防止缩小的类型注释过度指定了它们。

你有什么:

export class AddSuccessAction {
    public readonly type: string = ADD_SUCCESS;

    constructor(public payload: Mission) {}
}

export class AddFailedAction {
    public readonly type: string = ADD_FAILED;

    constructor(public payload: string) {}
}

你想要什么:

export class AddSuccessAction {
    readonly type = ADD_SUCCESS;

    constructor(public payload: Mission) {}
}

export class AddFailedAction {
    readonly type = ADD_FAILED;

    constructor(public payload: string) {}
}

一个工作switch

switch (action.type) {
  // action.type is `"[Mission] AddSuccess" | "[Mission] AddFailed"`
  // action.payload is `string | Mission`
  case missionActions.ADD_SUCCESS:
    // action.type is `"[Mission] AddSuccess"`
    // action.payload is `Mission`
    const mission = action.payload;
}

为什么这很重要:

字符串文字类型是区分联合可能性的一种常见且惯用的方式,但是通过将实现属性声明为 type string,这是所有字符串文字类型的超类型,我们禁止类型推断,从而防止缩小。请注意,这string不是可以缩小范围的类型。

一般来说,当一个值有一个初始化器时,最好利用 TypeScript 的类型推断。除了启用预期的场景之外,您还会对类型推断器将捕获的错误数量印象深刻。我们不应该告诉编译器比它需要知道的更多,除非我们故意想要一个比它推断的更通用的类型。有了--noImplicitAny它,当我们需要以类型注释的形式指定额外信息时,它总会让我们知道。

评论:

const您可以通过将文字值指定为或readonly属性的类型,在技术上指定类型并仍然保留缩小行为。然而,这增加了维护成本并且相当多余。

例如,以下是有效的:

export const ADD_FAILED: "[Mission] AddFailed" = "[Mission] AddFailed";

但你只是在不必要地重复自己。

在他们的回答中,ilyabasiuk 提供了一些很好的参考链接,这些链接是关于使用 ngrx 的文字类型以及它在最近的 TypeScript 语言迭代中是如何演变的。

要了解为什么在不可变位置推断文字类型,请考虑它支持强大的静态分析,从而更好地检测错误。

考虑:

type Direction = "N" | "E" | "S" | "W";

declare function getDirection(): Direction;

const currentDirection = getDirection();

if (currentDirection === "N") { // currentDirection is "N" | "E" | "S" | "W"

}
// Error: this is impossible, unreachable code as currentDirection is "E" | "S" | "W"
// the compiler knows and will give us an error here.
else if (currentDirection === "N") {

}
于 2017-11-01T20:00:24.440 回答
2

这是对迁移的非常好的描述,您正在尝试执行https://github.com/ngrx/example-app/pull/88#issuecomment-272623083

您必须进行以下更改:

  • 删除动作常量字符串的类型定义;
  • 删除类型属性的类型定义;
  • 如果您的 tslint 配置为不允许错过类型定义,则在文件开头添加 /* tslint:disable:typedef*/(是的,它看起来不太好,但如果我必须在操作文件中类型定义的强规则之间进行选择,则支持减速器的类型我肯定会选择第二个);

所以你会得到类似的东西:

/* tslint:disable:typedef*/
export const ADD_SUCCESS = "[Mission] AddSuccess";
export const ADD_FAILED = "[Mission] AddFailed";

export class AddSuccessAction implements Action {
   public readonly type = ADD_SUCCESS;

   constructor(public payload: Mission) {}
}

export class AddFailedAction implements Action {
   public readonly type = ADD_FAILED;

   constructor(public payload: string) {}
}

// More actions are defined here but im not gonna copy all

...

export type Actions =
    AddSuccessAction |
    AddFailedAction;

Aluan Haddad 在上面的帖子中很好地解释了为什么它如此有效。

于 2017-11-02T08:14:24.493 回答