基础篇
前置知识
学习TS的三招心法
- TS是JS的超集,其实就是TS是JS的语法糖而已。
- TS是基于JS的一门强类型的高级语言,但TS的所有语法和类型校验都只在编译环境有效。
- TS核心的功能就是,值的类型化。
值的类型化是什么意思
什么是值?
凡是可以被变量存储的单元都是值
它可以是简单的字符串、数字,也可以是复杂的由多条语句组成的代码单元,如类、函数、对象等
// 字符串作为值
let stringValue = 'hello world';
// 对象字面量作为值
let objectLiteralValue = {
attribute: 'hello world'
};
// 函数作为值
let funcValue = function(){};
// 函数返回作为值
function fn(){
return 'hello world';
}
let returnValue = fn();
// 类作为值
let classValue = class {}
所谓值的类型化,就是说每一个值都一定是某种或某几种特定类型
在TypeScript中,所有的值都具有强制类型,值的类型化是TypeScript区别于JavaScript最显著的特征。
实际上,JavaScript也是有类型的,只不过JavaScript的类型信息是在编译阶段由编译器判定,对程序员来说,值可以任意赋予,编写代码时就好像类型弱的不存在一样。
Typescript如何判断值的类型
类型注解
冒号加类型构成了类型注解,冒号前后可以包含任意空格。
// 声明字符串类型变量
let str: string;
// 声明字符串类型变量并初始化
let strValue: string = 'hello world';
// 指定类的属性类型
class Hello {
show: boolean = true;
}
// 指定函数参数和返回值类型
function sum(a: number, b: number): number {
return a + b;
}
类型推导
类型注解是可选的,在绝大多数未显式注解类型的情况下,编译器能自动推导出值的类型
// 变量被自动推导为字符串类型 string
let variable = 'hello world';
// 等价于
let variable: string = 'hello world';
// 返回值被自动推导为数字类型 number
function show(param: number) {
return param;
}
// 等价于
function show(param: number): number {
return param;
}
类型查询
类型查询是一条语句,相当于一个独立类型。
代码中任何需要显式注解类型的地方,都可以使用类型查询代替。
// 声明a为number
let a: number;
// 通过类型查询声明b的类型
let b: typeof a;
// 等价于
let b: number;
// 函数fn为函数
function fn(){}
// 通过类型查询声明d的类型为fn的类型
let d: typeof fn;
// 等价于
let d: () => void;
总结
类型注解
、类型推导
和类型查询
构成了TypeScript的类型判定系统,TypeScript编译器判定值的类型时,主要是通过以上三种方式。
TS 支持的类型
// 简单类型
number
boolean
string
symbol
void // 表示没有类型或空类型
undefined
null
never
any
// 复合类型
object // {}
array // 普通数组 T[] 元组 [T0, T1, T2 ....]
function // let a = function() {}; let b: typeof a; b <==> () => void
enum
联合类型 T1 | T2
交叉类型 T1 & T2
class
interface
undefined & null
Null类型和Undefined类型的类型表现和编译选项strictNullChecks
有关
// ------ strictNullChecks: true ------
// 合法
let n: null = null;
let u: undefined = undefined;
let v: void = undefined;
// 不合法
let v: void = null;
// ------ strictNullChecks: false ------
// 合法
let v1: undefined = null;
let v2: void = undefined;
let v3: null = undefined;
let x: number = null;
let y: string = undefined;
never
- 一个从来不会有返回值的函数,(如:如果函数内含有
while(true) {}
) - 一个总是会抛出错误的函数,(如:
function foo() { throw new Error('Not Implemented') }
,foo
的返回类型是never
)
这跟void区别在哪?因为function test(): void {}
void
是没有类型,而never
是永不存在的值的类型- 一个函数没有返回值时,它返回了一个
void
类型,一个函数根本就没有返回值时(或者总是抛出错误),它返回了一个never
void
只能为他赋值undefined
(strictNullChecks: true),never
只能赋值为never
来自【深入理解 TypeScript】
function类型
定义:(p1: T1, p2: T2, ...) => T
// 直接定义的函数
function sum(a: number, b: number): number {
return a + b;
}
// 通过typeof关键字获取函数sum的类型为:
// (a: number, b: number) => number;
let fn: typeof sum;
函数兼容
判断一个函数类型是否和一个函数兼容,只需判断参数类型和返回值类型是否同时兼容
// 声明fn为函数类型
let fn: (x: number, y: string) => boolean;
// 正确,参数名字不做兼容检查
fn = function(a: number, b: string): boolean {
// ...
return true;
}
// 错误,函数返回值类型不匹配
fn = function(a: number, b: string): string {
// ...
return b;
}
可选参数
// 参数a必须,参数b可选,b必须位于a之后
function test(a: number, b?: number): void {
// ...
}
enum 枚举类型
enum T { ... },对枚举值显式初始化,那么枚举值默认为数字类型,值也可以设置成字符串类型
enum Direction {
Up, // 值默认为 0
Down, // 值默认为 1
Left, // 值默认为 2
Right // 值默认为 3
}
// 如果添加数字默认值
enum Direction {
Up = 2,
Down,
Left = 3.3
Right
}
Direction.Up === 2; // 显式初始化
Direction.Down === 3; // 2 + 1 = 4.3
Direction.Left === 3.3; // 显式初始化
Direction.Right === 4.3; // 3.3 + 1 = 4.3
// 字符串
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
联合类型 & 交叉类型
联合类型: T1 | T2, 表示
值
可能是类型T1
或者T2
,类似或
// 类型与类型联合
let a: string | number = '3';
// 值与值联合
let b: '3' | '4' | 5 = 5
// 值与类型的联合
let c: '3' | number = '3'
交叉类型: T1 & T2,表示
值
是类型T1
和T2
的合集,类似且
type Ta = {
key: string
}
type Tb = {
value: number
}
let c: Ta & Tb = {
key: '3',
value: 4
}
// 等价于
// type Tc = {
// key: string
// value: number
// }
// 不合法
let c: Ta & Tb = {
key: '3'
}
tips
keyof
关键字作用于类型,通过获取一个类型的所有属性名,生成一个新的字符串值的联合类型
interface Ia {
key: string,
value: number,
name: string,
code: number
}
let c: keyof Ia = 'code'
// 等价于
// type c = 'key' | 'value' | 'name' | 'code'
类类型
类本身就是一种类型
// 定义类
class TypeA {
// ...
}
// 声明TypeA类型
let a: TypeA;
// 赋值TypeA类型
a = new TypeA();
Typescript里的Class 和 ES6的class
主要是区别在于属性和静态属性的区别
// ES6合法的定义
class Greeter {
constructor(){
// 正确,ES6中实例属性只能定义在构造器内部
this.greeting = 'world';
}
}
// 正确,ES6中静态属性只能定义在类外部
Greeter.greeting = 'world';
// 下面是TypeScript代码
class Greeter {
// 定义实例属性并初始化
greeting: string = 'world';
// 定义静态属性并初始化
static greeting: string = 'world';
}
访问控制 public/private/protected 修饰符
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
// 错误: 不能在类外访问私有成员
// error TS2341: Property 'name' is private and only accessible within class 'Animal'
new Animal("Cat").name;
consle.log(Animal.name) // Cat
protected
修饰符与private
修饰符的行为相似,都不能在类的外部访问。但有一点不同,protected
成员可以在派生类中访问
// 定义基类
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
// 定义派生类
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name)
this.department = department;
}
public getElevatorPitch() {
// 正确,来自父类的name成员在派生类里可以访问。虽然它位于Persion类的外部
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// 错误,受保护的成员 name 不能在外部访问
// error TS2445: Property 'name' is protected and only accessible within class 'Person' and its subclasses
console.log(howard.name);
tips
class Animal {
// private修饰符用于构造器的name参数前
constructor(private name: string) {}
}
// 以上定义相当于
class Animal {
private name: string;
constructor(name: string) {
this.name = name;
}
}
只读修饰符 readonly
class Octopus {
readonly name: string;
// 在属性声明时初始化
readonly numberOfLegs: number = 8;
constructor (theName: string) {
// 在构造函数里初始化
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
// 错误! name 是只读的
// error TS2540: Cannot assign to 'name' because it is a constant or a read-only property
dad.name = "Man with the 3-piece suit";
tips 只读属性和常量类似,一旦初始化,之后就再也不允许被赋值。
抽象类和抽象方法
用于描述抽象类和抽象方法的关键字都是 abstract,抽象方法没有方法体
一般情况下,抽象类和抽象方法是同时出现的,但也有例外:
- 一个类包含抽象方法,那么这个类必须是抽象类
- 抽象类可以没有抽象方法
// Animal是抽象类
abstract class Animal {
// makeSound是抽象方法,没有方法体
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
抽象类主要是用来被继承使用,抽象方法必须在派生类中必须被实现
// Animal是抽象类
abstract class Animal {
// makeSound是抽象方法,没有方法体
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
// 错误,抽象方法makeSound没有实现
// error TS2515: Non-abstract class 'Dog' does not implement inherited abstract member 'makeSound' from class 'Animal'
class Dog extends Animal {
// 空类
}
// 正确,抽象方法被实现
class Dog extends Animal {
makeSound(): void{
// ...
}
}
接口类型
对象类型是TypeScript的类型系统中最复杂也是最重要的类型,对象类型主要用来描述复杂数据类型
// 声明一个值为对象字面量
let man = {name: 'joye', age: 30};
// 等价于
let man: {name: string; age: number} = {name: 'joye', age: 30};
对象类型是匿名的接口类型,对象类型没有名字,接口类型有名字。接口类型相当于为对象类型声明了一个别名
// 定义接口类型Person
interface Person {
name: string;
age: number;
}
// 声明变量 man 为 Person 接口类型
let man: Person = {name: 'joye', age: 30};
// 等价于
let man: {name: string; age: number} = {name: 'joye', age: 30};
总结:
接口代表接口类型,匿名接口代表对象类型
可选属性 & 只读属性
interface Person {
name: string;
// 注意此处的问号,age此时为可选属性
age?: number;
// 只读属性
readonly key: string
}
接口的应用
接口最重要的作用在于描述一个复杂值的外形
,通常情况下,接口可以描述:
- 对象字面量
- 函数
- 可索引值
- 类
对象字面量
interface Person {
name: string;
age: number;
}
// 定义一个对象字面量male
let male = {
name: 'joye',
age: 30,
gender: 'male'
};
// 正确,male包含Person接口的所有属性
let man: Person = male;
// 不正确
// error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
let man: Person = {
name: 'joye',
age: 30,
gender: 'male'
};
对象字面量在直接赋值的时候,编译器会检查字面量类型是否完全匹配
描述函数
// 描述函数
interface MyFunc {
(name: string, age: number): string;
}
// 声明接口类型
let fn: MyFunc;
// 等价于
let fn: { (name: string, age: number): string; } // 匿名接口
// 等价于
let fn: (name: string, age: number) => string;
怎么判断两个类型是否兼容或者说等价?
让他们相互赋值
let fn1: MyFunc;
let fn2: (name: string, age: number) => string;
fn1 = fn2; // OK
fn2 = fn1; // OK
这里还有些坑?当在interface 含有protected
和private
成员,我们在进阶篇讲
描述可索引值
// 描述一个数组
interface StringArray {
[index: number]: string;
}
// 声明接口类型
let myArray: StringArray;
// 等价于
let myArray: { [index: number]: string; }; // 匿名接口
// 等价于
let myArray: string[];
// 赋值
myArray = ["Bob", "Fred"];
描述对象
// 描述一个对象
interface MyObject {
[index: string]: string;
}
// 声明接口类型
let myObject: MyObject;
// 赋值
myObject = {
a: '1',
b: '2',
c: '3'
}
描述类数组对象
// 类数组对象
let obj = {
1: 1,
2: 2,
name: 'ddd',
age: 30
}
obj[1] === 1;
obj[2] === 2;
obj['name'] === 'ddd';
obj['age'] === 30;
描述类
// 定义一个类
class NewClass {}
// 用接口来描述这个类类型
interface MyClass {
new(): NewClass;
}
// 声明一个变量为描述这个类的接口类型并初始化
let myClass: MyClass = NewClass;
// 等价于
let myClass: typeof NewClass = NewClass;
我们介绍到用接口来描述函数、可索引值、类类型,你会发现还不如直接用类型来声明更直接
// 声明函数
let myFunc: ()=>{};
// 声明数组
let myArr: string[];
// 声明类
class MyClass {}
let myClass: typeof MyClass;
用类来实现接口
interface Person {
height: string;
weight: string;
run(): void;
}
class Man implements Person {
height = '180cm';
weight = '70kg';
run() {
console.log('run')
}
}
实现类必须包含接口所声明的全部必选属性
接口继承
接口既可以继承接口,也可以继承类。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
// 正确,color 属性来自父接口
let square: Square = {
color: 'blue',
sideLength: 4
};
接口继承类
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
class Size {
size: string = '20cm'
}
interface CustomSqure extends Shape, Size {
sideLength: number;
}
// 正确,color 属性来自父接口
let square: Square = {
color: 'blue',
sideLength: 4
};
// 正确, 若不加size 就会报错
let mySquere: CustomSqure = {
color: 'blue',
sideLength: 4,
size: '10cm'
}