接口初探
接口 - 可选属性 + 只读属性
接口 - 额外属性检查 + 函数类型 + 可索引的类型
额外属性检查
本来是color,但是错写成了color1,是要报错的,但是如果采用下面的方式,用一个中间变量,无论是编辑器还是编译器都检查不出错误,这就很容易造成bug,如果你真的希望能支持color1属性,那么你应该修改 SquareConfig,或者索引签名,而不是这样绕过。
虽然SquareConfig
里的属性都是可选属性(Optional Properties),但这只意味着接口实例里可以没有这个的属性,并不意味着可以多出其他的属性。检查是否有不在接口定义中的属性,就是额外的属性检查。
由此我们可以看到,TypeScript中额外的属性检查只会应用于对象字面量场景,所以,在TS的官方测试用例里面,我们看到的都是objectLiteralExcessProperties.ts。
用变量的情况下,即使他是类似于function printLabel(labeledObj: LabeledValue)
这样函数中的一个参数,也不会触发额外属性检查,因为他会走另一个逻辑:类型兼容性
回到上面的例子,在定义myObj
的时候,并没有指定它的类型,所以TS会推断他的类型为{ size: number; label: string; }
。当他作为参数传入printLabel
函数时,ts会比较它和LabelledValue
是否兼容,因为LabelledValue
中的label属性的,myObj
也存在,所以他们是兼容的,这就是最上面提到的鸭式辨型法。
interface LabelledValue {
label: string;
}
let labeledObj: LabelledValue;
// myObj的推断类型是{size: number; label: string;}
let myObj = {size: 10, label: "Size 10 Object"};
// 兼容,myObj可以赋值给labeledObj
labeledObj = myObj;
TS的作者ahejlsberg是这样描述这个fresh的问题,核心思想就3点:
- 每个对象字面量在初始化的时候都被认为是新鲜(fresh)的
- 当一个新鲜的对象字面量在赋值给一个非空类型的变量,或者作为一个非空类型的参数时,如果这个对象字面量里没有那个非空类型中指定的属性,就会报错
- 在类型断言后,或者对象字面量的类型被拓展后,新鲜度会消失,此时对象字面量就不再新鲜
用一个例子来说明
interface A {
a: number;
b: string;
}
const test = {
a: 10,
b: "foo",
c: "bar"
}
const a: A[] = [test];
const b: A[] = [{
a: 10,
b: "foo",
c: "bar" // ❌ not assignable type error
}];
const c: A[] = [{
a: 10,
b: "foo",
c: "bar"
} as A];
const d: A[] = [test, {
a: 10,
b: "foo",
c: "bar"
}];
const e: A[] = [{
a: 10,
b: "foo",
c: "bar" // ❌ not assignable type error
}, {
a: 10,
b: "foo",
c: "bar"// ❌ not assignable type error
}];
上面这个例子,a和b就是刚刚讨论的变量不进行额外属性检查问题。
c中我们对新鲜的对象字面量进行了断言操作,所以新鲜度消失,不会进行额外属性检查。
d中,因为有test这个变量的存在,而test又因为赋值时进行了类型推断,推断成一个跟A兼容的类型。因此, 在一个字面量数组中,根据最佳通用类型的推断,对象字面量的类型被拓展成了一个跟A兼容的类型,新鲜度也消失了,不会进行额外属性检查,赋值也成功了。
最后一个e,两个都是新鲜的对象字面量,没有发生类型推断,所以新鲜度没有消失,会触发额外属性检查
函数类型:
interface SearchFunc {
(source: string, subString: string): boolean
}
let mySearch: SearchFunc
mySearch = function(source: string, subString: string): boolean {
let result = source.search(subString)
return result > -1
}
其实,定义函数的时候,既然前面已经指定类型了,就不用再给参数、返回值写类型了,会自动推断。
interface SearchFunc {
(source: string, subString: string): boolean
}
let mySearch: SearchFunc
mySearch = function(source, subString) {
let result = source.search(subString)
return result > -1
}
数字索引 + 字符串索引
继承接口
interface Shape {
color: string
}
interface PenStroke {
penWidth: number
}
interface Square extends Shape, PenStroke {
sideLength: number
}
let squere = {} as Square
squere.color = 'blue'
squere.sideLength = 10
squere.penWidth = 2.0
console.log(squere)
混合类型
// 混合类型,就是一个东西,既可以是函数,也可以是对象,等等,axios就是充分利用了这一点
interface Counter {
(start: number): string // 函数签名
// 除此之外,还希望它能作为一个对象
interval: number
reset(): void
}
function getCounter(): Counter {
let counter = (function(start: number) {}) as Counter
counter.interval = 1000
counter.reset = function() {}
return counter
}
let c = getCounter()
c(10)
c.reset()
c.interval = 500
console.log(c)
接口继承类
当一个接口继承类的时候,会继承类的私有成员,那么定义类的时候,就要实现这个私有成员,一个子类,只有继承了父类,才能去实现里面的接口。接口继承类,使用场景不是很多。
类 - 基本示例 + 继承
但是,继承后不能一直叫Animal啊,得有具体的动物名字
class Animal {
name: string
constructor(name: string) {
this.name = name
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m`)
}
}
class Snake extends Animal {
constructor(name: string) {
// 利用super调用父类的构造函数
super(name)
}
move(distance: number = 5) {
// 蛇自有的一些行为console出来
console.log('Slithering...')
// 继承了父类里的move并调用
super.move(distance)
}
}
class Horse extends Animal {
constructor(name: string) {
super(name)
}
move(distance: number = 45) {
console.log('Galloping...')
super.move(distance)
}
}
let sam = new Snake('Sammy')
let tom = new Horse('Tommy')
sam.move(11)
tom.move(66)
类 - 公共、私有与保护修饰符 + readonly修饰符
默认都是public,可以省略不写。
如果成员是private,类的成员在类外不能被使用
存取器
抽象类 - abstract
抽象类通常作为其他派生类的父类,一般不能直接被实例化,抽象类里可以包含抽象方法,抽象方法是不能直接被实现的,需要在其派生类中实现。也可以包含成员方法,成员方法可以有自己的实现细节。
抽象类前 加 abstract;
抽象方法前 加 abstract;
上面的d是Department类型,该类型上不存在generateReports方法。除非把Department类型换成AccountingDepartment类型。
abstract class Department {
name: string
constructor(name: string) {
console.log('name: ', name)
this.name = name
}
printName():void {
console.log(`Department name: ${this.name}`)
}
// 定义一个抽象方法,只是一个函数签名,具体实现只能在派生类中
abstract printMeeting(): void
}
class AccountingDepartment extends Department {
constructor() {
// super直接调用了父类的构造函数,同时传递了参数
super('Accounting ad Auditing')
}
printMeeting(): void {
console.log('Each Monday at 10 am')
}
generateReports():void {
console.log('Generating accountin reports...')
}
}
let d: AccountingDepartment = new AccountingDepartment()
d.printName()
d.printMeeting()
d.generateReports()
高级用法之修改静态属性
class Greeter {
static standardGreeting = 'hello, there'
greeting: string
constructor(msg?: string) {
this.greeting = msg
}
greet() {
if (this.greeting) {
return `hello, ${this.greeting}`
} else {
return Greeter.standardGreeting
}
}
}
let greeter: Greeter
greeter = new Greeter()
console.log(greeter.greet()) // hello, there
// 重新创建一个新的Greeter类型的构造器,使用类类型,而不是实例类型,可以访问到静态变量
let greeterMaker: typeof Greeter = Greeter
greeterMaker.standardGreeting = 'modified there...'
let g2: Greeter = new greeterMaker()
console.log(g2.greet())
类作为接口使用
现在把基类的interface换成class,也是可以的,但是不建议这样使用。
基本示例 + 函数类型
function add(x: number, y: number): number {
return x + y
}
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
return x + y
}
// 上面等价于下面,不用写那么复杂,ts会自己推断
let myAdd1 = function(x: number, y: number): number {
return x + y
}
// let adrd = () => {}
myAdd(1, 2)
可选参数 + 默认参
JS经常搞arguments,TS里不玩这个。
如果遇到多个参数,你也不知道多少个参数,怎么搞呢?
function buildName(firstName: string, ...restNames: string[]): string {
console.log('restNames: ', restNames)
return firstName + ' ' + restNames
}
let buildNameFn: (fname: string, ...rest: string[]) => string = buildName
buildName('a', 'b', 'c')
this + 重载
let deck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createCardPicker: function() {
console.log('this1: ', this) // 指向 deck 对象本身
return function() {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
console.log('this2: ', this) // 指向global或者window全局对象
return {
suit: this.suits[pickedSuit], // Error
card: pickedCard % 13
}
}
}
}
// this:谁调用,就指向谁,所以,下面会报错
let cardPicker = deck.createCardPicker() // deck调用的,所以this1指向 deck 本身
let pickedCard = cardPicker() // window/global 调用的,所以this2指向 window/global
console.log('card: ' + pickedCard.card + ' of ' + pickedCard.suit)
重载
JS是动态语言,可以重载:根据不同参数返回不同类型
泛型
// 泛型基本示例
// 我们想让参数类型和返回类型是一样的,可以使用 类型变量
// 定义一个类型变量
function test<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
// 类型推断,根据传入的参数类型,自动确定了T类型,可以保持代码精简性,但是复杂情况,编译器一时推断不出来,还是得写完整
let i = test(['str'])
// 泛型基本示例
// 我们想让参数类型和返回类型是一样的,可以使用 类型变量
// 定义一个类型变量
function test<T>(arg: T): T {
return arg
}
let myTest: <U>(arg: U) => U = test
// 也可以用字面量形式
let myTest2: {<T>(arg: T): T} = test
泛型 - 泛型类 + 泛型约束
类型推断
基础、最佳通用类型、上下文类型
类型推断发生在 初始化变量、设置默认参数、决定参数返回值的时候。
zoo被推断为(Bee | Lion)[] 联合体。当然你可以手动修改矫正。明确声明为Animal类型。
还有一种推断方式叫上下文类型
交叉类型
// 交叉类型:多个类型合并为一个类型
function extend<T, U>(first: T, second: U): T & U {
let result = {} as T & U
for (let id in first) {
result[id] = first[id] as any
}
for (let id in second) {
if (!result[id].hasOwnProperty(id)) {
result[id] = second[id] as any
}
}
return result
}
class Person {
constructor(public name: string) {}
}
interface Loggable {
log(): void
}
class ConsoleLogger implements Loggable {
log(): void {}
}
// jim 就是交叉类型了,既包括Person里的name,也包括ConsoleLogger里的log函数
let jim = extend(new Person('jim'), new ConsoleLogger())
console.log(jim.name)
jim.log()
联合类型
function padLeft(value: string, padding: any) {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if (typeof padding === 'string') {
return padding + value
}
throw new Error(`Expected string or number got ${padding}`)
}
// 因为上面 padding 是any,实际上太宽泛了
// 下面这个,不会报错,因为是any,但是运行就会报错
padLeft('hello world', true)
// 可以用联合类型解决这个问题,也就是只允许 string 或者 number
function padLeft1(value: string, padding: string | number) {}
上面只能调用共有方法。我们可以用类型保护的手段来解决这样的问题。
interface Bird {
fly()
layEggs()
}
interface Fish {
swim()
layEggs()
}
function getSmallPet(): Fish | Bird {}
let pet = getSmallPet()
pet.layEggs()
pet.swim()
// 如何判断是否有该函数
if (pet.swim) {
pet.swim()
} else if (pet.fly) {
pet.fly()
}
// 上面的写法每次访问属性都会报错
// 可以使用类型断言
if ((pet as Fish).swim) {
(pet as Fish).swim()
} else if ((pet as Bird).fly) {
(pet as Bird).fly()
}
// 但是你看写的太麻烦了,可以使用 类型谓词
// 看个例子,判断是否Fish,传的参数是个pet,Fish | Bird 的联合类型
// 可以容错,作为一种 保护机制
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
// 上面的各种as可以利用 类型谓词 的保护机制进行改写
if (isFish(pet)) {
pet.swim()
} else {
// 这种判断很智能,else走的不是Fish类型,就自动推断出pet是Bird自动提示fly和layEggs
pet.fly()
}
// 再看一个例子
function isNumber(x: any): x is number {
return typeof x === 'number'
}
function isString(x: any): x is string {
return typeof x === 'string'
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(' ') + value
}
if (isString(padding)) {
return padding + value
}
throw new Error('类型错误')
}
class Bird {
fly() { console.log('bird fly') }
layEggs() { console.log('bird lay eggs') }
}
class Fish {
swim() { console.log('fish swim') }
layEggs() { console.log('fish lay eggs') }
}
function getrandomPet(): Fish | Bird {
return Math.random() > 0.5 ? new Bird() : new Fish()
}
let pet = getrandomPet()
if (pet instanceof Bird) {
pet.fly()
} else {
pet.swim()
}
总结:类型保护包括三种:类型谓词(is)、typeof、instanceof
可以为null的类型 + 字符串字面量类型
上图中,编译器是没法识别的, 编译阶段识别不出来,认为name有可能为null
字符串字面量
指定的字符串必须有确定的值,可以和联合类型、类型保护综合使用。
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
// 联合类型