关于ts的一些问题

如下代码,is用法我看的文章都是传一个参数,然后尝试用两个,遇到几个疑问

  1. 发现下图的isP函数不管第一个参数是true还是false,liney的c都是string,linez都是number,
  2. 如果我把if部分放到外面,即使用{}包起来,c会提示是never类型,放到函数里c就是联合类型
  3. 假设变量m是布尔,n是联合类型o|p,当m为true,n是o类型,否则是p类型,那么在不用as断言的情况下如何根据m来收窄n的类型

    const isP = (a: boolean, b: number | string): b is string => {
      return a == true;
    };
    let c: number | string = 3;
    const t = () => {
      if (isP(true, c)) { // x
     console.log(c.length); // y
      } else{
     console.log(c.toFixed(2)); // z
      }
      console.log(c);
    };
阅读 1.4k
2 个回答
const isP = (a: boolean, b: number | string): b is string => {
  return a == true;
};

对于第一条:
这个函数的意思是:如果第一个参数a是真值,那么b是一个字符串类型的变量。因此isP(true, c)进入到if的真分支,这里c认为是一个字符串,所以可以去访问length属性。反之假分支中c认为是一个number类型的值,可以使用toFixed方法。

对于第二条:
c放到函数t里面声明,c仅存活于t函数中,可以明确知道要么对c进行了string类型的赋值,要么进行了number类型的赋值,所以可以直接推断c的类型。let c: number | string = 3;这句话一旦赋值,c暂时就是number类型,isP(true, c)会导致c明明是number,非说他是字符串,就产生了never。

对于第三条:
最好还是不要用值来决定或影响类型。变量的值是运行时的概念,ts的类型是编译期的概念。联合类型想要取其一,要么是类型系统帮你推断好是什么类型(c放在函数t里面声明赋值),要么就是没法预知类型,只能靠代码逻辑判断类型(c放在函数t外面声明赋值)。对于前者,是因为类型系统可以帮你推测出来,你不要在过多的担心。但是对于后者,因为c可能是不可预知的,比如是从某个接口里读的数据,接口声明返回值也是联合类型的,这你怎么判断,还是得靠代码逻辑才能进入对的分支。不知道这么说有没有覆盖你的场景。但是真的别把变量的值和变量的类型揉在一起。

const isP = (a: boolean, b: number | string): b is string => { ... }

// 类似的函数声明
function isP(a: boolean, b: number | string): b is string { ... }

从这个声明来看,如果这个函数的返回值是 true,那么 b 就是 string 类型。这是一个类型断言函数。然后

if (isP(predicate, c)) {
    // true 分支中
}

根据 if 的语法,这里可以断定返回值为 ture 的时候会进入「true 分支」。再根据 isP 的声明,返回值为 true 的时候 b 形参,也就是 c 实参,是 string。所以这里「true 分支」代码块中的 c 都会以 string 类型来进行推断运算。

然而

let c: number | string = 3;
if (isP(true, c)) { ... }

这里虽然声明了 c 是联合类型,但实际它的值是 number,甚至可以更准确的说是 3 这个字面类型(字面类型可以在静态类型检查阶段参与类型运算)。在 isP() 之前没有对它进行不确定类型的赋值,所以编译器能检查出来 c 的类型和 isP 类型断言冲突,没办法,只能给个 never 告诉你“这不可能”。定义函数内的 if 分支,因为函数的执行时机不确定,所以 c 在运行当时的值和类型都不能确定,所以只能按 isP 断言来分析。上面的语句改一下,让 c 的值类型变得不确定:

let c: number | string = 3;
function test(x: number | string) : number | string {
    return x;
}

c = test(c);
if (isP(true, c)) { ... }

现在 c 的值并没有变化,但是它的类型变成了真正的 number | string,因为编译器不会去分析 test() 内部逻辑对 c 的影响,仅从函数接口声明中分析出来它的类型是函数的返回类型。

其实用强制类型转换也可以做到

let c: number | string = 3 as number | string;
//                         ^^^^^^^^^^^^^^^^^^
if (isP(true, c)) { ... }

这里的 3 就不再是字面类型 3 了,而是 number | string 类型,后面的 isP 不会产生断言冲突。

最后,很重要的一点 —— 由于 isP 的实现并没有准确地分析出来 b 参数的类型,算是类似欺骗(或者算 Bug),可以骗过编译器,可惜骗不过运行时,会在运行时发生错误(比如 number 没有 .length 属性,这里运行时会报错)。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题