这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战
今天来学习TypeScript中的接口、类和函数,它们到底是什么。
接口
简单例子
TypeScript的核心原则之一是对值所具有的结构进行类型检查,在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
先看一个简单的例子
interface IProps {
name: string;
}
const logName = (props: IProps) => console.log(props.name);
logName({ name: 'xiong' })
// logName({ name: 'xiong', age: 18})
// ===>. 类型“{ name: string; age: number; }”的参数不能赋给类型“IProps”的参数。
// 对象文字可以只指定已知属性,并且“age”不在类型“IProps”中
我们定义了一个函数logName用来打印我们传入的对象中的属性name的值,传入logName中的参数必须包含name属性,并且他的类型是string,除此之外不能传入其他多余的属性,这就是我们一个基本的接口。当我们定义多个属性时,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
可选属性
接口里的属性不全都是必需的。在不同的情况下有不同的参数传入,不会全部传入,那么有些参数就是可选的,我们只需要在参数后面加问号?就是说明该参数可选。
interface IProps {
name: string;
age?: number
}
说明该对象必须有name属性,不一定有age属性,如果有,那么必须是number类型。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性。
interface IProps {
name: string;
age?: number
readonly $: number;
}
let user: IProps = { name: 'John', $: 1 }
user.name = 'change'
// error
user.$ = 2 // 无法分配到 "$" ,因为它是只读属性。
数组同样可以是只读的,TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
额外的属性检查
interface IProps {
name: string;
readonly $: number;
}
// 不能将类型“{ name: string; $: number; age: number; }”分配给类型“IProps”。
// 对象文字可以只指定已知属性,并且“age”不在类型“IProps”中。
let user: IProps = { name: 'John', $: 1, age: 10 }
当我们多传了一个参数进去,ts会给我们报错。TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
那么如何绕开这个错误了,第一种就是类型断言
let user: IProps = ({ name: 'John', $: 1, age: 10 } as IProps)
第二种就是添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。接口IProps可能会有任意数量的其它属性,我们可以这样定义
interface IProps {
name: string;
readonly $: number;
[key: string]: any;
}
当然,如果说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。最好该修改IProps定义来体现出这一点。
函数类型
接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface IFunc {
(name: string, age: number): string
}
const func: IFunc = (name: string, age: number) => {
return `${name} ${age}`
}
我们的函数func必须接受两个参数,第一个是string类型,第二个是number类型,返回值是string类型,而形参可以不与接口定义中相同。
可索引的类型
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
他表示我们用 number去索引StringArray时会得到string类型的返回值。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
类类型
实现接口
他和Java里面的接口基本一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Clock类必须实现ClockInterface接口中所有方法和属性。接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
继承接口
和类一样,我们的接口也可以继承, 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface IName {
name: string;
}
interface IAge {
age: number;
}
interface IUser extends IName, IAge {
$: number
}
const user: IUser = { name: 'xiong', age: 18, $: 100 }
混合类型
接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
类
传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,而TypeScript是允许我们基于类的继承并且对象是由类构建出来的。
简单例子
我们定义了一个Greeter类,他有三个成员,分别是 greeting属性,构造函数和 greet方法。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
在类中用了 this, 它表示我们访问的是类的成员。当我们使用 new构造了 Greeter类的一个实例时, 它会调用之前定义的构造函数,创建一个 Greeter类型的新对象,并执行构造函数初始化它。
继承
class Animal {
name: string;
move() {
console.log('Animal move');
}
}
class Dog extends Animal {
move() {
console.log('Dog move');
}
}
let dog = new Dog();
dog.move()
这个例子是最基本的继承:类从基类中继承了属性和方法。Dog类是一个派生类,Animal类是基类,通过 extends关键字,他继承了Animal类的属性和方法。如果我们要调用父类的方法需要使用super,在构造函数中给父级传参也是super(name)
公共,私有与受保护的修饰符
公共修饰符public
默认我们的属性和方法都是public的,在任何地方都可以访问
私有修饰符private
当成员被标记成 private时,它就不能在声明它的类的外部访问
保护修饰符 protected
protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。
readonly修饰符
同样,我们的类可以和接口一样,对某个属性只读,不需修改
静态属性
在之前,我们只说了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。我们只需要加上static标识符就好了。想要访问该属性或方法时,直接类名.名称就好了。
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。
抽象类中的抽象方法必须在派生类中实现,抽象类不能被实例化