THE LAST TIME
The last time, I have learned
【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是给自己的查缺补漏和技术分享。
笔者文章集合详见:
前言
JavaScript
毋庸置疑是一门非常好的语言,但是其也有很多的弊端,其中不乏是作者设计之处留下的一些 "bug"。当然,瑕不掩瑜~
话说回来,JavaScript
毕竟是一门弱类型语言,与强类型语言相比,其最大的编程陋习就是可能会造成我们类型思维的缺失(高级词汇,我从极客时间学到的)。而思维方式决定了编程习惯,编程习惯奠定了工程质量,工程质量划定了能力边界,而学习 Typescript,最重要的就是我们类型思维的重塑。
那么其实,Typescript
在我个人理解,并不能算是一个编程语言,它只是 JavaScript
的一层壳。当然,我们完全可以将它作为一门语言去学习。网上有很多推荐 or 不推荐 Typescript 之类的文章这里我们不做任何讨论,学与不学,用或不用,利与弊。各自拿捏~
再说说 typescript(下文均用 ts 简称),其实对于 ts 相比大家已经不陌生了。更多关于 ts 入门文章和文档也是已经烂大街了。此文不去翻译或者搬运各种 api或者教程章节。只是总结罗列和解惑,笔者在学习 ts 过程中曾疑惑的地方。道不到的地方,欢迎大家评论区积极讨论。
其实 Ts 的入门非常的简单:.js
to .ts
; over!
但是为什么我都会写 ts 了,却看不懂别人的代码呢? 这!就是入门与进阶之隔。也是本文的目的所在。
首先推荐下 ts 的编译环境:typescriptlang.org
再推荐笔者收藏的几个网站:
- Typescript 中文网
- 深入理解 Typescript
- TypeScript Handbook
- TypeScript 精通指南
下面,逐个难点梳理,逐个击破。
可索引类型
关于ts 的类型应该不用过多介绍了,多用多记 即可。介绍下关于 ts 的可索引类型。准确的说,这应该属于接口的一类范畴。说到接口(interface),我们都知道 **ts 的核心原则之一就是对值所具有的结构进行类型检查。**它有时被称之为"鸭式辩型法"或"结构性子类型"。而接口就是其中的契约。可索引类型也是接口的一种表现形式,非常实用!
ini
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上面例子里,我们定义了StringArray
接口,它具有索引签名。 这个索引签名表示了当用number
去索引StringArray
时会得到string
类型的返回值。 Typescript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
这是因为当使用number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
scala
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
下面的例子里,name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
typescript
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
当然,我们也可以将索引签名设置为只读,这样就可以防止给索引赋值
typescript
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
interface 和 type 关键字
stackoverflow 上的一个高赞回答还是非常赞的。typescript-interfaces-vs-types
interface
和 type
两个关键字的含义和功能都非常的接近。这里我们罗列下这两个主要的区别:
interface
:
- 同名的
interface
自动聚合,也可以跟同名的class
自动聚合 - 只能表示
object
、class
、function
类型
type
:
- 不仅仅能够表示
object
、class
、function
- 不能重名(自然不存在同名聚合了),扩展已有的
type
需要创建新type
- 支持复杂的类型操作
举例说明下上面罗列的几点:
Objects/Functions
都可以用来表示 Object
或者 Function
,只是语法上有些不同而已
typescript
interface Point{
x:number;
y:number;
}
interface SetPoint{
(x:number,y:number):void;
}
typescript
type Point = {
x:number;
y:number;
}
type SetPoint = (x:number,y:number) =>void;
其他数据类型
与 interface
不同,type
还可以用来标书其他的类型,比如基本数据类型、元素、并集等
ini
type Name = string;
type PartialPointX = {x:number;};
type PartialPointY = {y:number;};
type PartialPoint = PartialPointX | PartialPointY;
type Data = [number,string,boolean];
Extend
都可以被继承,但是语法上会有些不同。另外需要注意的是,interface 和 type 彼此并不互斥。
interface extends interface
typescript
interface PartialPointX {x:number;};
interface Point extends PartialPointX {y:number;};
type extends type
ini
type PartialPointX = {x:number;};
type Point = PartialPointX & {y:number;};
interface extends type
typescript
type PartialPointX = {x:number;};
interface Point extends PartialPointX {y:number;};
type extends interface
ini
interface ParticalPointX = {x:number;};
type Point = ParticalPointX & {y:number};
implements
一个类,可以以完全相同的形式去实现interface
或者 type
。但是,类和接口都被视为静态蓝图(static blueprints) ,因此,他们不能实现/继承 联合类型的 type
typescript
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x: 1;
y: 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x: 1;
y: 2;
}
type PartialPoint = { x: number; } | { y: number; };
// FIXME: can not implement a union type
class SomePartialPoint implements PartialPoint {
x: 1;
y: 2;
}
声明合并
和 type
不同,interface
可以被重复定义,并且会被自动聚合
ini
interface Point {x:number;};
interface Point {y:number;};
const point:Pint = {x:1,y:2};
only interface can
在实际开发中,有的时候也会遇到 interface
能够表达,但是type
做不到的情况:给函数挂载属性
ini
interface FuncWithAttachment {
(param: string): boolean;
someProperty: number;
}
const testFunc: FuncWithAttachment = function(param: string) {
return param.indexOf("Neal") > -1;
};
const result = testFunc("Nealyang"); // 有类型提醒
testFunc.someProperty = 4;
& 和 | 操作符
这里我们需要区分,|
和 &
并非位运算符 。我们可以理解为&
表示必须同时满足所有的契约。|
表示可以只满足一个契约。
ini
interface IA{
a:string;
b:string;
}
type TB{
b:number;
c:number [];
}
type TC = TA | TB;// TC 的 key,包含 ab 或者 bc 即可,当然,包含 bac 也可以
type TD = TA & TB;// TD 的 可以,必须包含 abc
交叉类型
交叉类型,我们可以理解为合并。其实就是将多个类型合并为一个类型。
Man & WoMan
- 同时是 Man 和 Woman
- 同时拥有 Man 和 Woman 这两种类型的成员
r
interface ObjectConstructor{
assign<T,U>(target:T,source:U):T & U;
}
以上是 ts 的源码实现,下面我们再看一个我们日常使用中的例子
typescript
interface A{
name:string;
age:number;
sayName:(name:string)=>void
}
interface B{
name:string;
gender:string;
sayGender:(gender:string)=>void
}
let a:A&B;
// 这是合法的
a.age
a.sayGender
注意:16446
ini
T & never = never
extends
extends
即为扩展、继承。在 ts 中,extends 关键字既可以来扩展已有的类型,也可以对类型进行条件限定 。在扩展已有类型时,不可以进行类型冲突的覆盖操作。例如,基类型中键a
为string
,在扩展出的类型中无法将其改为number
。
typescript
type num = {
num:number;
}
interface IStrNum extends num {
str:string;
}
// 与上面等价
type TStrNum = A & {
str:string;
}
在 ts 中,我们还可以通过条件类型进行一些三目操作:T extends U ? X : Y
typescript
type IsEqualType<A , B> = A extends B ? (B extends A ? true : false) : false;
type NumberEqualsToString = IsEqualType<number,string>; // false
type NumberEqualsToNumber = IsEqualType<number,number>; // true
keyof
keyof 是索引类型操作符 。用于获取一个"常量"的类型,这里的"常量"是指任何可以在编译期确定的东西,例如const
、function
、class
等。它是从 实际运行代码 通向 类型系统 的单行道。理论上,任何运行时的符号名想要为类型系统所用,都要加上 typeof
。
在使用class
时,class
名表示实例类型,typeof class
表示 class
本身类型。是的,这个关键字和 js 的 typeof
关键字重名了 。
假设 T 是一个类型,那么keyof T
产生的类型就是 T
的属性名称字符串字面量类型构成的联合类型(联合类型比较简单,和交叉类型对立相似,这里就不做介绍了)。
注意!上述的 T 是数据类型,并非数据本身。
ini
interface IQZQD{
cnName:string;
age:number;
author:string;
}
type ant = keyof IQZQD;
在 vscode
上,我们可以看到 ts
推断出来的 ant
:
注意,如果 T
是带有字符串索引的类型,那么keyof T
是 string
或者number
类型。
索引签名参数类型必须为 "string" 或 "number"
typescript
interface Map<T> {
[key: string]: T;
}
//T[U]是索引访问操作符;U是一个属性名称。
let keys: keyof Map<number>; //string | number
let value: Map<number>['antzone'];//number
泛型
泛型可能是对于前端同学来说理解起来有点困难的知识点了。通常我们说,泛型就是指定一个表示类型的变量,用它来代替某个实际的类型用于编程,而后再通过实际运行或推导的类型来对其进行替换,以达到一段使用泛型程序可以实际适应不同类型的目的。说白了,泛型就是不预先确定的数据类型,具体的类型在使用的时候再确定的一种类型约束规范。
泛型可以应用于 function
、interface
、type
或者 class
中。但是注意,泛型不能应用于类的静态成员
几个简单的例子,先感受下泛型
c
function log<T>(value: T): T {
console.log(value);
return value;
}
// 两种调用方式
log<string[]>(['a', ',b', 'c'])
log(['a', ',b', 'c'])
log('Nealyang')
- 泛型类型、泛型接口
r
type Log = <T>(value: T) => T
let myLog: Log = log
interface Log<T> {
(value: T): T
}
let myLog: Log<number> = log // 泛型约束了整个接口,实现的时候必须指定类型。如果不指定类型,就在定义的之后指定一个默认的类型
myLog(1)
我们也可以把泛型变量理解为函数的参数,只不过是另一个维度的参数,是代表类型而不是代表值的参数。
typescript
class Log<T> { // 泛型不能应用于类的静态成员
run(value: T) {
console.log(value)
return value
}
}
let log1 = new Log<number>() //实例化的时候可以显示的传入泛型的类型
log1.run(1)
let log2 = new Log()
log2.run({ a: 1 }) //也可以不传入类型参数,当不指定的时候,value 的值就可以是任意的值
类型约束,需预定义一个接口
scss
interface Length {
length: number
}
function logAdvance<T extends Length>(value: T): T {
console.log(value, value.length);
return value;
}
// 输入的参数不管是什么类型,都必须具有 length 属性
logAdvance([1])
logAdvance('123')
logAdvance({ length: 3 })
泛型的好处:
- 函数和类可以轻松的支持多种类型,增强程序的扩展性
- 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性
- 灵活控制类型之间的约束
泛型,在 ts 内部也都是非常常用的,尤其是对于容器类非常常用。而对于我们,还是要多使用,多思考的,这样才会有更加深刻的体会。同时也对塑造我们类型思维非常的有帮助。
本文由mdnice多平台发布