最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 浅谈 Typescript(三):两个空间的交流

    正文概述 掘金(HenryLulu_几木)   2021-08-18   471

    上一篇我们了解了 TS 在「类型声明空间」的行为,那「类型声明空间」的产物是如何约束「变量声明空间」的,「变量声明空间」又能为「类型声明空间」提供哪些信息呢?这就是本篇要讨论的——两个空间的交流。

    本文你将看到:

    • 「类型声明空间」如何为「变量声明空间」的声明提供类型注解
    • 哪些场景下TS会“自动注解”?推断的规则又是怎样的?
    • 如果变量已有或推断的类型不准确,我们可以修正吗?又可能存在什么问题?
    • TS 怎样通过 JS 逻辑语句自动缩小类型范围?
    • 我们如何从「变量声明空间」提取类型声明?

    1 类型注解

    从「类型声明空间」到「变量声明空间」,最基础的,我们可以通过类型注解,为变量提供类型约束。如下图所示,我们上一篇在「类型声明空间」构造出的任何产物,都可以直接拿来注解。

    浅谈 Typescript(三):两个空间的交流

    基本注解

    一个冒号是最基本的注解方式,它完成了从类型声明空间向变量声明空间的映射。

    // 直接注解原始类型
    const foo: number = 1;
    // 注解已声明类型
    type bar = number;
    const foo: bar = 1;
    // 用类型表达式注解
    const baz: number | string = 1;
    // 用接口表达式注解
    const obj: {
    	foo: number;
    	bar: string;
    } = { foo: 1, bar: 'string' };
    

    函数注解

    先声明类型再注解

    上一篇我们说到两种声明函数类型的方式:

    // 声明调用模式
    interface Foo {		// 或者 type Foo =
      (bar: string): number;
    };
    // 声明函数类型
    type Foo = (bar: string) => number;
    

    然后,以字面量声明的函数变量就可以直接注解,这很符合前面冒号注解的风格。

    const getLen: Foo = (input) => input.length;
    

    但这样有个问题,就是你在函数声明这里,没法直观的看到输入输出的类型,可读性就差了那么一点点。

    直接注解到函数声明上

    所以更好的办法是直接在声明处注解。比如对于上述字面量声明,也可以直接注解上去:

    const getLen = (input: string): number => input.length;
    

    这样我们就能在函数声明行清清楚楚看到输入输出的类型,特别当你不想手写返回类型(想依赖类型推论)的时候,就没必要单把参数类型写到别的地方了。这种方式在函数表达式中也同样适用:

    function getLen(input: string): number {
      return input.length;
    }
    

    或者,我可以让某几个参数可选

    const getLen = (input: string, options?: any): number => input.length;
    

    重载

    直接注解到函数声明上的另外一个好处是,方便我们扩展函数的重载。

    很多情况下,我们的函数不只有一种传参形式,比如类似 CSS 里的padding,我可以给 1个数、2个数、4个数是吧,那我怎么声明呢?

    function padding(a: number, b?: number, c?: number, d?: number): string => {...};
    

    这里有几个问题:

    1. 这种声明方式允许我传三个参数,那么类型约束就不够严谨;
    2. 因为传参数量不同的时候,参数的意义也不同,在别人调用我函数的时候,我就没法通过参数名明确告诉他每个参数是什么意思,只能 abcd 这么笼统地来。

    padding 的例子:

    // 重载的三种声明头
    function padding(all: number);
    function padding(topAndBottom: number, leftAndRight: number);
    function padding(top: number, right: number, bottom: number, left: number);
    // 实际用的头,且不作为类型注解向外暴露
    function padding(a: number, b?: number, c?: number, d?: number) {
      if (b === undefined && c === undefined && d === undefined) {
        b = c = d = a;
      } else if (c === undefined && d === undefined) {
        c = a;
        d = b;
      }
      return {
        top: a,
        right: b,
        bottom: c,
        left: d
      };
    }
    

    2 类型推断

    在 JS 中,变量创建是非常频繁的,创建方式也五花八门,如果我每次搞到一个变量都要通过注解的方式给它类型,不是要累死?值得高兴的是,TS考虑到了这个问题,在很多场景下,它会根据你「变量声明空间」的变量流转逻辑,帮你“猜”出新变量的类型。

    从右向左

    这么好的事,哪些场景可以自动推断呢?——一切可以从赋值和变量流转追溯到类型的地方。所谓的赋值和流转,离不开等号,TS会尝试从右侧推断出左侧的类型。

    直接赋值

    当变量创建的时候就赋值,TS便可以由赋值概括出它的类型。

    const foo = 1;	// foo 的类型被推断为 1
    

    那如果我只声明不赋值也不注解会怎么样?变量会被推断为 any,这对一个类型系统是无意义的,TS 会提醒你不要这么做。

    结构化

    这种「从赋值推断类型的能力」是可以嵌套的。如果你给变量赋了一个复杂的值,TS也会基于基础类型的判断,嵌套下去,归纳出一个类型:

    const bar = {		// bar 的类型被推断为接口 { baz: string; }
    	baz: '',
    };
    

    数组也是一种结构化数据:

    const foo = [ 1, '2' ];	// foo 被推断为 (string | number)[]
    

    可能你有疑问,为什么 foo 不被推断为[ number, string ]的元组?这涉及TS推论的方法,叫做“最佳通用类型”。

    相反,解构也会推断类型,以数组为例:

    const foo = [ 1, 2 ];
    const bar = foo[0];	// bar 被推断为 number
    

    运算

    如果等号右边是一个运算表达式,TS同样可以推断出变量的类型:

    const foo = 1 + 1;	// foo 被推断为 number
    const bar = 1 + '1';	// bar 被推断为 string,TS 甚至知道强制类型转换的规则
    

    但我们要说的运算不止于此,任何已知类型变量的运算结果都可以被推断:

    let foo: number;
    let bar: string;
    const baz = foo + bar;	// baz 被推断为 string
    

    函数

    从某种意义上说,函数赋值也是一个结构化的定义。函数类型中的“返回值”部分,可以像结构化一样推断出来。

    const foo = () => {	// foo 被推断为 () => number
    	return 1;
    };
    

    同样的,返回值也可以依赖内部变量类型或者参数类型推断出来。

    const foo = (bar: number) => {	// foo 被推断为 () => number
    	const baz = 2;
    	return bar + baz;
    };
    

    但函数内部的情况会更复杂,所以出现了一些针对函数返回值的特殊类型,诸如 void、never。

    const foo = () => {	// foo 没有返回,被推断为 () => void
    	doSomething();
    };
    const bar = () => {	// bar 总执行不完,被推断为 () => never
    	throw new Error();
    };
    

    Error

    类型推断除了用于确定未注解变量的类型,也用于及时判断出不合理的类型赋值。比如:

    const foo: number = '';	// Error: 不能将类型“string”分配给类型“number”
    

    从左向右

    相反,如果等号左侧已经确定了类型,右侧的赋值也会吸收左侧的类型,并尝试约束自己的行为。由于右侧的值与所处上下文强相关,这种特性叫做“按上下文归类”。比如这样一个函数:

    let foo: (bar: number) => number; // 已经注解 foo 的类型
    foo = (bar) => {		// 这里的函数赋值会根据所处上下文,吸收 foo 的已定类型,并解析到参数和返回值中
    	return bar.length	// Error: 类型“number”上不存在属性“length”
    };
    

    关于包装对象

    另外一个点是关于包装对象的。我们知道在「变量声明空间」创建一个字符串值有几种方法。不同声明方法默认推断成不同类型:string 或 String。

    var foo = new String("Avoid newing things where possible");	// String
    var bar = "A string, in TypeScript of type 'string'";		// string
    var baz = String('aaa');	// string
    

    string 是 ts 原始类型值,String 则指向 es5.d.ts 中定义的 interface,你可以想象到它的实现方式:

    // es5.d.ts
    interface String {
    	valueOf(): string;	
    }
    interface StringConstructor {
    	new (value): String;
    	(value): string;
    }
    declare const String: StringConstructor;
    

    只能把 string 赋值给 String,不能把 String 赋值给 string。

    foo = bar	// 正常
    bar = foo	// 不能将类型“String”分配给类型“string”。“string”是基元,但“String”是包装器对象。如可能首选使用“string”。
    

    3 类型断言

    是的,我比编译器懂得更多,我清楚地知道一个实体具有比它现有类型更确切的类型。我需要一种方式,让我推翻编译器的推断。这就叫“类型断言”。as<>都能用来做断言:

    const foo: any;
    (foo as string).length;
    <string>foo.length;
    

    类型断言通常用在:缩小类型范围、

    断言的限制

    浅谈 Typescript(三):两个空间的交流

    如果不加以限制,断言就成了“指鹿为马”,随意破坏 TS 的类型环境。所以断言只在部分合理的场景下可用,具体来说:

    • 如果 A 类型兼容 B 类型,那么 A、B 可以相互断言
    • 顶层类型(any / unknown)可以和任意类型相互断言
    • 联合类型可以和任意子集相互断言

    其中关于兼容,TS 这样解释的,说白了就是不管类型怎么定义的,只要一方满足另一方定义的所有属性,就是兼容的。

    下面我们回到正题,通过以下几个例子看下断言的限制:

    let foo: number;				foo as any;						// ok, 顶层类型
    let foo: any; 				foo as number;					// ok, 顶层类型
    let foo: number | string; 	foo as number;					// ok, 联合类型
    interface Parent { p: string }
    interface Child extends Parent { c: number }
    let foo: Parent;				foo as Child;					// ok, 兼容
    let foo = [1, 'a'];			foo as [number, number];		// ok, 联合类型
    
    let foo: number;				foo as string;					// 不 ok, 指鹿为马
    interface Parent { p: string }
    interface Child { c: number }
    let foo: Parent;				foo as Child;					// 不 ok, 不兼容
    

    双重断言

    这就是断言的风险,我先把类型断言为一个宽松的中间类型,又断言到一个本来不可以直接断言到的类型,仍然可以达到“指鹿为马”的效果。

    let foo = 1;	foo as string;							// 不 ok,类型 "number" 到类型 "string" 的转换可能是错误的,如果这是有意的,请先将表达式转换为 "unknown”。
    let foo = 1;	foo as number | string as string;		// ok, 联合类型成了中间类型
    

    但在上面第一句的报错中,TS 也提到“如果这是有意的,请先将表达式转换为 "unknown””。可见TS对这个风险是充分知晓的,甚至是如 any 一样有意留的“后门”,而在双重断言中,作为「中间类型」的更常见也更方便的是 any 和 unknown。但不论怎样,我们应尽量减少双重断言的使用。

    4 类型保护

    我们品一品这里发生了什么,秦二世首先判断了联合类型「鹿|马」变量传入的值是否存在「角」这个属性,然后类型断言为「马」,最后调用了「马」的方法,说得通吧。

    const whatQinIIThink = (ani: Deer|Horse) => {
    	if (!('角' in ani)) {
    		const hor = (ani as Horse)
    		hor.ride();
    	}
    }
    

    事实上,TS 更加智能,当!('角' in ani)满足时,就自动把「鹿」从 ani 的联合类型中刨出去了,不需要手动断言缩小范围。

    这种特性叫类型保护:在某些 JS 语句下,尽可能把类型保护在更小范围更精确的类型中。

    触发类型保护的方式

    哪些JS语句可以触发类型保护呢?

    // 1. typeof
    function doSome(x: number | string) {
      if (typeof x === 'string') {
        // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
        console.log(x.substr(1)); // ok
      }
    }
    
    // 2. instanceof
    class Foo { foo = 123 }
    class Bar { bar = 123 }
    function doStuff(arg: Foo | Bar) {
      if (arg instanceof Foo) {
        console.log(arg.foo); // ok
        console.log(arg.bar); // Error
      }
    }
    
    // 3. in
    interface A {x: number}
    interface B {y: string}
    function doStuff(q: A | B) {
      if ('x' in q) {
        // q: A
      }
    }
    
    // 4. 字面量
    type Foo = {	kind: 'foo'; // 字面量类型	};
    type Bar = {	kind: 'bar'; // 字面量类型	};
    function doStuff(arg: Foo | Bar) {
      if (arg.kind === 'foo') {
        console.log(arg.foo); // ok
        console.log(arg.bar); // Error
      }
    }
    

    自定义的类型保护

    但上面的语句只能进行简单判断,有时候情况更复杂。

    这时,只要把判断函数的返回值声明为foo is Type形式,调用时就会触发TS的类型保护。

    function isDeer (ani: Deer|Horse): ani is Deer {
    	return ('角' in ani) && ('花纹' in ani) && !('鬃' in ani);
    }
    const whatQinIIThink = (ani: Deer|Horse) => {
    	if (!isDeer(ani)) {
    		hor.ride();		// ok
    	}
    }
    

    小结

    在从「类型声明空间」向「变量声明空间」做注解这件事上,TS做了很多纠结又恰到好处的决定。兼顾了类型系统的严格性和开发的灵活性,权衡了“自动”带来的效率和准确度。

    5 类型捕获

    最后,我们再来看下,类型如何从「变量声明空间」反向流转到「类型声明空间」。这种行为叫做类型捕获。

    浅谈 Typescript(三):两个空间的交流

    类型捕获很简单,是通过 typeof 实现的。

    let foo = 123;
    let bar: typeof foo; // 'bar' 类型与 'foo' 类型相同(在这里是: 'number')
    bar = 456; // ok
    bar = '789'; // Error: 'string' 不能分配给 'number' 类型
    

    有些情况下,类型捕获还会和上一篇提到的类型运算结合:

    const colors = {
      red: '1',
      blue: '2'
    };
    type Colors = keyof typeof colors;	// 'red' | 'blue'
    

    小结

    本篇我们讨论类型在两个空间之间的交流

    • 「变量声明空间」的声明产物都可以注解为「类型声明空间」的类型
    • 函数可以通过重载注解多种参数形式,实现调用的灵活准确
    • TS 会为声明的变量推断类型,推断的依据来自于对变量赋值、运算、流转的追溯
    • 在有限的场景下,开发者也可以通过类型断言,进行已有类型注解或推断的修正
    • TS 会根据 JS 条件语句,自动缩小联合类型的范围,进行类型保护
    • 如果需要从变量反向取出它的类型,用 typeof 捕获就好

    起源地下载网 » 浅谈 Typescript(三):两个空间的交流

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元