类型推论
如果没有明确的指定类型,那么ts会依照类型推论的规则推断出一个类型
1. ts更严格
虽然ts是js超集,但并不意味着所有js的用法都能照搬,js可以跨类型给变量赋值,但是ts不行。
//这种写法ts会报错,js是可以的
let str="lengyuexin";
str=1024
2. ts推论结果
ts根据第一次变量赋值的类型进行推导,实际上,上述代码和下边代码是等价的
//这个string类型就是ts根据第一次变量赋值的类型进行推导得到的结果
let str:string="lengyuexin";
str=1024
3. 定义但不赋值
如果变量定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
//这不是什么好习惯,像极了脱缰的野马
//当你试图削弱ts静态类型检查优势的时候,说明你依旧过度依赖js写法
let str;
str="lengyuexin"
str=1024;
str=[]
str=false;
str={}
联合类型
联合类型表示取值可以为多种类型中的一种,不同类型使用管道符|分隔,有点像或的感觉。
1. 使用示例
let age:string|number;
age=18;//ok
age="18"//ok
age=false//error ,非联合类型子类型的不可以
2. 访问联合类型的属性和方法
这有点像交集,只有共有的属性和方法才可以被访问
//这样代码会报错,number类型是没有length属性的
function getLength(something: string | number): number {
return something.length;
}
//但是这样就可以,字符串和数组都有length属性
function getLength(something: string | number[]): number {
return something.length;
}
//也可以像这样访问公共方法
function getStr(something: string | number[]): string {
return something.toString();
}
3. 联合类型的类型推论
类型推论的规则对于联合类型依旧适用
let age: string | number;
age = '20';
console.log(age.length); // 正常运行
age = 20;// 第一次赋值,age已经被ts视为string
console.log(age.length); // Property 'length' does not exist on type 'number'.
类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型,ts中的断言有以下两种使用方式:
- <类型>值
- 值 as 类型 在tsx(React jsx的ts版)语法中必须用这种
1. js中的断言
console.assert(expression, message),expression为false情况下,会在控制台打印message
console.assert(true,"hello")//正常运行,无输出
console.assert(false,"hello")//VM2587:1 Assertion failed: hello
2. ts中的断言
联合类型的变量尚未确定类型的时候,只能访问联合子类型共有的属性或方法,断言可以突破这个限制
//这样会报错
function getLength(something: string | number): number {
if (something.length) {
return something.length;
} else {
return something.toString().length;
}
}
//用断言就ok
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}
//类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的
// 这样会报错,boolean不属于联合类型string | number
function toBoolean(something: string | number): boolean {
return <boolean>something;
}
内置对象
ts核心库定义文件定义了所有浏览器环境需要用到的类型,内置类型判断,NodeJS的需要安装@types/node
1. esma
Boolean、Error、Date、RegExp 等。
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
//...
2. bom/dom
Document、HTMLElement、Event、NodeList 等。
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
3. 内置类型检查
//报错 e被推断成 MouseEvent,而 MouseEvent 没有 targetCurrent 属性
document.addEventListener('click', function(e) {
console.log(e.targetCurrent);
});
类与接口
上一篇文章对对象类型进行约定的时候使用了接口,实际上,接口还可以用于对类的某个行为进行抽象。而至于类,可用于对接口的特定规范进行实现,也就是常说的某接口的实现类。类可以继承(单继承),接口也可以(多继承),面向对象的思想在ts中体现的更明显。
1. 类实现接口
接口可以是类某一行为的抽象,类可以实现多个接口。将动物视为一个类,哺乳动物为动物的子类。猫和狗都属于哺乳动物,都具有发出叫声这一行为。而这一行为,就可以抽象为接口。
//定义动物类 若有实例属性外部初始化,需要构造函数
class Animal {
color: string;
constructor(color: string) {
this.color = color;
}
//也可以声明实例方法
yell(str:string):string{
return str
}
}
//*******************//
// 以下示例基于最原始的 class Animal {}
//将叫声这一行为抽象成接口
interface AnimalYell{
yell():void
}
//定义动物类
class Animal {
}
// 定义Mammal类,继承Animal类,实现AnimalYell接口
class Mammal extends Animal implements AnimalYell {
yell() {
console.log('动物发出叫声');
}
}
//Dog,Cat类继承父类Mammal,重写yell方法
// 这里也可以直接让Dog,Cat实现AnimalYell接口
//之所以继承,是更大程度的复用父类Mammal的特点(属性和方法)
class Dog extends Mammal {
yell(){
console.log('汪汪...');
}
}
class Cat extends Mammal {
yell(){
console.log('喵喵...');
}
}
// 一个类可以实现多个接口
interface AnimalWalk{
walk():void
}
//Snake实现AnimalWalk,AnimalYell两个接口\
//实现接口要实现每个接口中的方法
//若以下代码缺少yell或walk的实现就会报错
class Snake implements AnimalWalk,AnimalYell{
yell(){
console.log("嘶嘶...")
}
walk(){
console.log("爬行...")
}
}
new Mammal().yell()//动物发出叫声
new Dog().yell()//汪汪...
new Cat().yell()//喵喵...
new Snake().yell()//嘶嘶...
new Snake().walk()//爬行...
2. 接口继承接口
类可以继承,接口也可以。实际上,ts中接口还能继承类,这在其他面向对象语言中并不允许(如java)。
//将手机功能抽象为一个接口
interface BasePhoneFunction {
call(): string; //打电话
send(message: string): string[]; //群发短信
}
// 在基础功能上,还有其他功能
interface PhoneFunction extends BasePhoneFunction {
game(): void; //打游戏
listen(): void; //听音学
watchTV(...tvName:string[]): void; //看电视
note(): string[]; //便签
}
class Phone implements PhoneFunction {
call() {
return "hi, tom,when are we going to coding?";
}
send(message: string) {
console.log(message);
return ["tom", "jack", "july"];
}
game(){
console.log("打游戏")
}
listen(){
console.log("听音乐")
}
//注意这里的rest参数用法,形参名不必和接口定义的形参名一致 tvName
watchTV(...args:string[]){
console.log(`看${args.toString()} `)
}
note(...args:string[]){
return [...args]
}
}
const p=new Phone()
console.log(p.call())//hi, tom,when are we going to coding?
p.send("新年快乐") //新年快乐
console.log(p.send("新年快乐"))// 新年快乐 ["tom", "jack", "july"]
p.game()//打游戏
p.listen()//听音乐
p.watchTV("琅琊榜","将夜")//看琅琊榜,将夜
console.log(p.note("吃饭","睡觉","打豆豆"))//[ '吃饭', '睡觉', '打豆豆' ]
3. 接口继承类
这看起来有些不可思议,接口怎么能继承一个类呢?ts毕竟是ts有它的特殊性,就像js对象不支持反向映射,但是ts的枚举本质就是对象,就能支持反向映射(基于数字的枚举)
//注意,这种写法不能缺少构造函数
class Person {
eat: string;
drink: string;
constructor(eat:string,drink:string){
this.eat=eat
this.drink=drink
}
}
//接口PersonAction 继承类Person , 代码正常运行
//接口PersonAction能有什么用呢?定义对象啊
interface PersonAction extends Person {
play: string;
}
const p: PersonAction={
eat:"酸菜鱼",
drink:"可乐",
play:"coding"
}
3.1 接口继承类的本质
实际上,接口能继承类是因为ts中类具有特殊性,既是用于实例化操作的类,又是某种类型规范
class Person {
eat: string;
drink: string;
constructor(eat:string,drink:string){
this.eat=eat
this.drink=drink
}
}
//这是最常规的用法了
new Person("酸菜鱼","可乐")
//还可以将其作为一种类型,定义对象
const p:Person={
eat:"酸菜鱼",
drink:"可乐"
}
之前定义对象都是用的接口,现在可以用类,这说明ts中的类和接口存在共性。回到最初,接口继承类,来探寻答案
class Person {
eat: string;
drink: string;
constructor(eat:string,drink:string){
this.eat=eat
this.drink=drink
}
}
//其实这种写法等价于下边这种写法
interface PersonAction extends Person {
play: string;
}
//=>step1, 注意看BasePerson和Person的异同
interface BasePerson {
eat: string;
drink: string;
}
//=>step2
interface PersonAction extends BasePerson {
play: string;
}
//这样依旧ok,Person类实际被拆解成了接口
const p:PersonAction={
eat:"酸菜鱼",
drink:"可乐",
play:"coding"
}
3.2 同与不同
经过上述探索,得出结果:ts接口继承类本质是接口继承接口。但是被转换成接口的类(BasePerson),和原始的类(Person),差异在哪?再看一个例子。
class Person {
//实例属性
eat: string;
drink: string;
constructor(eat:string,drink:string){
this.eat=eat
this.drink=drink
}
//实例方法
log():void{
console.log("log...")
}
//静态属性
static age:number=18;
//静态方法
static log():void{
console.log("static log...")
}
}
console.log(Person.age)//18
Person.log()//static log...
// 该类被接口继承的时候 发生的变化
interface BasePerson {
eat: string
drink: string
log():void
}
- 没有构造函数。 这其实很正常,定义类型规范而已,又不是实例化,要构造函数干嘛?
- 只有实例属性和实例方法会被保留, 静态属性或静态方法不被继承
访问修饰符
java中访问修饰符4种(public/protected/default/private),ts中的访问修饰符3种(public/protected/private)
1. public
public是权限最高的,默认就是public,可自由的访问程序里定义的成员
// 默认写法
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 等价于这种写法
class Person {
public name: string;
public constructor(name: string) {
this.name = name;
}
}
console.log(new Person("tom").name)//tom
2. private
class Person {
private name: string;
private constructor(name: string) {
this.name = name;
}
}
console.log(new Person("tom").name)//报错,外部访问不到
3. protected
class Person {
protected name: string;
protected constructor(name: string) {
this.name = name;
}
}
class Student extends Person {
constructor(name: string) {
super(name); //调用父类构造函数
}
getName(): string {
//Student是Person的子类,所以可以拿到protected修饰的name属性
return this.name;
}
}
console.log(new Student("tom").getName()); //tom
参考链接
- 掘金ts小册,https://juejin.im/book/5da08714518825520e6bb810
- ts中文手册,https://zhongsp.gitbooks.io/typescript-handbook
- ts入门教程,https://ts.xcatliu.com