本文是我在TypeScript在日常使用结合官方文档,参考掘金小册总结出来的觉得重要且较为基础的部分。
基础类型
TypeScript支持与JavaScript几乎相同的数据类型,主要说下不同的类型。
和JS相同的类型:boolean,number,string,null,undefined,Object,Array。默认情况下,类型检查器认为 null 与 undefined 可以赋值给任何类型。 null 与 undefined 是所有其它类型的一个有效值。tsconfig中--strictNullChecks 标记可以解决此错误。
不同的类型:
- any
可以表示任意类型,在不清楚变量的类型时,常用来摆脱类型检查。
- void
在JS中只有void操作符,void的作用便是返回undefined。在TS中,表示没有任何类型,常用来做函数的返回值。
- never
表示的是那些永不存在的值的类型,是任何类型的子类型,也可以赋值给任何类型,但是除了never任何类型都不能赋给never。在写类型系统中,有很大用处。
// 取得A、B相同的类型
export type SetIntersection<A, B> = A extends B ? A : never;
- unknown
这是TS3.0引入的新类型,是any类型相当安全的类型。虽然它们都可以是任意类型,但是unknown类型再被确定之前,它不能被当做对象进行任何操作比如实例化、getter、函数执行等等。
let a:any;
let u:unknown
a=1; //ok
u=1; //ok
a.foo//ok
u.ok//error
if (u instanceof Date) {
u.toISOString();//ok
}
- enum
使用枚举类型可以为一组常量赋予友好的名字。有三种类型,数字枚举、字符串枚举、异构枚举。
//默认是数字类型
enum Color {Red, Green, Blue}
//相当于
enum Color {Red=0, Green=1, Blue=2}
//字符串枚举
enum Color {Red="red", Green="green", Blue="blue"}
//异构枚举
enum Color {Red=0, Green="green", Blue="blue"}
本质他们就是数字和字符串,对于枚举类型,你向其赋值规定中的数值是不会报错的。在日常使用,如果常使用一组常量,比如1代表普通图,2代表高清图,就可使用枚举给这写常量赋予友好的名字,让后来的小伙伴知道你写的常量是什么意思,而且对于长的字符串,这样写有代码提示。
//如果是普通图就做一些处理
if(item.type === 1){....}
//下面的代码易读性就比上面的好,而且不用写注释
enum ImageType{Normal=1,HD=2}
if(item.type === ImageType.Normal){....}
变量声明
和ES6是一样的,在座的基础想必都很好,我就不介绍了,就再次推荐下解构赋值,蛮好用。
- 数组解构
let [first,second] = [1,2,3,4];
// 使用 ... 语法创建剩余变量
let [first,...rest] = [1,2,3,4];//first = 1,rest=[2,3,4]
- 对象解构
let p={name:"wangwang",vocal:"bangbang",action:"work"}
let {name,vocal,action:a} = p;//name = "wangwang",vocal="bangbang",a="work"
let {name,...rest} = p;
函数
TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
- 函数类型
// 可选参数
type funcType = (x: number, y: number, z?:number) => number;//其实也可以用接口写
// 默认参数
let func:funcType = function (x,y,z=1){
return x+y;
}
// 剩余参数
function add(x,...rest){
...
}
- 重载
JavaScript 里函数根据传入不同的参数而返回不同类型的数据,Ts就提供这种类型。也是提高了易读性、易用性。
let suits = ["hearts", "spades", "clubs", "diamonds"];
// 两个重载类型
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
//pickCard根据传入参数的不同会返回两种不同的类型
function pickCard(x): any {
// 具体实现
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
字面量
这个在官网里没有独立的章节描述,但挺多地方会提及这个概念,我们就简单讲一下。
// 字符串字面量
"1"
// 字符串变量
let a = "1";
没有赋值给变量的值就可以成为字面量,和TS的类型一样,它分为 真值字面量类型(boolean literal types),数字字面量类型(numeric literal types),枚举字面量类型(enum literal types),大整数字面量类型(bigInt literal types)和字符串字面量类型(string literal types)。
接下来,我们讲两个很绕口的概念:
字面量类型
let a:"a" = "a"; //ok
a = "b";//error
上个例子把"a"当做一种类型使用,规定变量只能是"a",把字面量当做类型就叫做字面量类型,很简单吧(* ̄︶ ̄)
类型字面量
let arr:[number,string]=["name",20];
这就是类型字面量,把类型当做字面量去使用定义新的类型。
接口
TypeScript的核心原则之一是对值所具有的结构进行类型检查。它有时被称做“鸭式辨型法”或“结构性子类型化”。简单来说,就是它不关心你有什么,它只在乎你有没用它想要的。TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。看官网的例子感受下:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
和函数一样属性也有不同的类型
interface SquareConfig {
// 可选类型
color?: string;
// 只读类型
readonly width: number;
// 普通类型
height: number;
}
属性检查
这个例子和接口的第一个例子很像,你们猜这种写法正确吗?
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
printLabel({ size: 10, label: "Size 10 Object" });
是会失败的,为什么呢?这里的函数实参变成对象字面量,对象字面量被赋值给变量或者作为参数的时候,会被特殊对待而且经过“额外属性检查”。如果一个对象字面量存在任何“目标类型”不包含的属性时,恭喜你,你就会获得一个错误。虽然不推荐绕开“额外属性检查”,但官网还是推荐了三种解决方法:
- 对象字面量赋给变量
//这里就是上文的写法,注意不要定义确定的类型啊,要不然还是会报错
let myObj = { size: 10, label: "Size 10 Object" };
- 类型断言
{ size: 10, label: "Size 10 Object" } as { label: string }
- 添加字符串索引签名
interface Label {
label: string
[propName: string]: any;
}
可索引类型
我们可以简单描述为那些能够“通过索引得到”的类型,比如 myArray[10] 或 ageMap['daniel']。
interface StringArray {
// [索引签名:对象索引的类型]: 索引返回值类型
[index: number]: string
}
let myArray: StringArray
myArray = ['Bob', 'Fred']
let myStr: string = myArray[0]
TS支持两种索引签名:字符串和数字。因为myArray[10]会被转成myArray["10"],数字转为string再去索引对象。所以数字索引的返回值必须是字符串索引返回值类型的子类型。
接口类型
- 函数类型
interface addFunc{
(a:number,b:number):number;
}
- 类类型
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
类是具有两个类型的:静态部分的类型和实例的类型。接口只能规定实例部分。constructor存在于类的静态部分,所以不在检查的范围内。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
ClockConstructor虽然并不能用来给类继承,但可以用来检查是否为可实例化的构造函数。
- 混合类型
先前我们提过,接口能够描述 JavaScript 里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。 比如下面这个例子,一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string
interval: number
reset(): void
}
function getCounter(): Counter {
let counter = (function (start: number) { }) as Counter
counter.interval = 123
counter.reset = function () { }
return counter
}
let c = getCounter()
c(10)
c.reset()
c.interval = 5.0
在使用第三方库,写.d.ts文件时,可能需要。
继承
简单的接口继承接口,就是从一个接口里复制成员到另一个接口里,不过多描述了,太常见了。
- 接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。但是这样这个接口类型只能被这个类或其子类所实现(implement)。
class Control {
private state: any;
}
// 接口继承类
interface SelectableControl extends Control {
select(): void;
}
// 正确使用
class Button extends Control implements SelectableControl {
select() { }
}
// 错误:“Image”不是Control的子类 缺少“state”属性。
class Image implements SelectableControl {
select() { }
}
类
对于有面向对象编程经验的人,这是很熟悉的语法,我就简单提下。
继承
派生类包含了一个构造函数,它必须调用 super(),它会执行基类的构造函数,而且必须在使用this之前。如果是继承的普通方法,也可以调用super,如super.move(),顺序没有要求。
公有、私有与受保护的修饰符
默认为public,private不允许本类以外范围访问,protect只允许自己和子类访问。readonly 关键字将属性设置为只读的。
TypeScript 使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。感受下
class Human{
public name:string;
}
class Dog{
public name:string;
}
let dog:Dog = new Dog(), human:Human;
human = dog; // ok
然而,当我们比较带有 private 或 protected 成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private 成员,那么只有当另外一个类型中也存在这样一个 private 成员,并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected 成员也使用这个规则。
class Human{
public name:string;
private think(){};
}
class Dog{
public name:string;
}
let dog:Dog = new Dog(), human:Human;
human = dog; // error
参数属性
参数属性通过给构造函数参数前面添加一个访问限定符来声明一个类的属性。使用 private 限定一个参数属性会声明并初始化一个私有成员;对于 public 和 protected 来说也是一样。
class Person {
constructor(readonly name: string) {
}
public talk(some:string){
console.log(this.name+some);
}
}
存取器
TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
静态属性
当类被实例化的时候才会被初始化的属性。 类的静态成员,这些属性存在于类本身上面而不是类的实例上。 提一下,如何创建一个静态成员不同的类。
class Greeter {
static standardGreeting = 'Hello, there'
}
console.log(Greeter.standardGreeting)
// 修改了静态属性,但是不影响Greeter的静态属性
let greeterMaker: typeof Greeter = Greeter
greeterMaker.standardGreeting = 'Hey there'
let greeter2: Greeter = new greeterMaker()
console.log(greeterMaker.standardGreeting)
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。
高级类型
- 交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。
- 联合类型
联合类型是从多个类型符合一个即可。比如一个变量希望传入 number或 string类型,let a:number|string = 1;
类型断言
有的TS不能正确或者准确推断类型,产生不必要的错误或警告。这个时候类型断言就可以上场了,有两种断言方式,<> 和 as。
let pet = getSmallPet();
(<Fish>pet).swim
(pet as Fish).swim
类型保护
假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet的类型,就不用使用类型断言了。TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
还可以用typeof、instanceof检查类型,做类型保护。
泛型
泛型是TS中非常重要的一个概念,给予开发者创造灵活、可重用代码的能力。先感受下泛型:
需要完成一个identity函数。 这个函数会返回任何传入它的值。
function identity<T>(arg: T): T {
return arg
}
函数名称后面声明泛型变量 ,当做是任意或所有类型,它用于捕获开发者传入的参数类型(比如说string),然后我们就可以使用T(也就是string)做参数类型和返回值类型了。
多个类型
定义泛型时,可以一次性定义多个类型参数
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
泛型变量
把泛型变量T当做类型的一部分使用,而不是整体,增加了灵活性。
function getArrayLength<T>(arg: Array<T>) {
console.log((arg as Array<any>).length) // ok
return arg
}
泛型类型
- 泛型函数
上面的几个例子都是泛型函数。
- 泛型接口
泛型也可以用于接口声明,以identity函数为例,我们把它改为泛型接口形式。
interface GenericIdentityFn<T> {
(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn<number> = identity
- 泛型类
泛型类看上去与泛型接口差不多。我们使用的Array和Map时,这些在TS中都是泛型类。
class GenericNumber<T> {
zeroValue: T
add: (x: T, y: T) => T
}
注意,无法创建泛型枚举和泛型命名空间。
泛型约束
有的时候我们并不希望泛型变量是什么类型都可以,要对有些条件,比如有 .length 属性的所有类型。这就是泛型约束。
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length) // OK
return arg
}
在泛型约束中使用类型参数
keyof,即索引类型查询操作符,我们可以用 keyof 作用于泛型 T 上来获取泛型 T 上的所有 public 属性名构成联合类型。
我们可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。
function getProperty<T, K extends keyof T> (obj: T, key: K ) {
return obj[key]
}
let x = {a: 1, b: 2, c: 3, d: 4}
getProperty(x, 'a') // okay
getProperty(x, 'm') // error
在泛型里使用类类型
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}