TypeScript: 基础

接口初探

接口 - 可选属性 + 只读属性

接口 - 额外属性检查 + 函数类型 + 可索引的类型

额外属性检查

本来是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点:

  1. 每个对象字面量在初始化的时候都被认为是新鲜(fresh)
  2. 当一个新鲜的对象字面量在赋值给一个非空类型的变量,或者作为一个非空类型的参数时,如果这个对象字面量里没有那个非空类型中指定的属性,就会报错
  3. 类型断言后,或者对象字面量的类型被拓展后,新鲜度会消失,此时对象字面量就不再新鲜

用一个例子来说明

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'
// 联合类型

猜你喜欢

转载自blog.csdn.net/GY_U_YG/article/details/124930305