自学TypeScript-基础、编译、类型

自学TypeScript-基础、编译、类型

  • TS 编译为 JS
  • 类型支持
    • 类型注解
    • 基础类型
    • `typeof` 运算符
    • 高级类型
      • class 类
        • 构造函数和实例方法
        • 继承
        • 可见性
        • 只读
      • 类型兼容性
      • 交叉类型
      • 泛型
        • 泛型约束
        • 多个泛型
        • 泛型接口
        • 泛型类
        • 泛型工具
      • 索引签名类型
      • 映射类型
      • 索引查询(访问)类型
    • 类型声明文件

TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,支持 ECMAScript 6 标准。因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译,将其编译成纯 JavaScript。

TS 和 JS 相比,增加的功能包括:

  • 类型批注和编译时类型检查
  • 类型推断
  • 类型擦除
  • 接口
  • 枚举
  • Mixin
  • 泛型编程
  • 名字空间
  • 元组
  • Await

并且有些功能从 ECMA2015 反向移植过来:

  • 模块
  • lambda 函数的箭头语法
  • 可选参数以及默认参数

因为 TS 是 JS 的超集,所以这里只研究与 JS 差别的部分

TS 编译为 JS

通常我们使用 .ts 作为 TypeScript 代码文件的扩展名。可以使用 tsc 命令将此文件编译为 JS 代码。例如

// app.ts
var message:string = "Hello World" 
console.log(message)
tsc app.ts
// app.ts 编译为 app.js
var message = "Hello World";
console.log(message);

当然因为绝大多数新版本的浏览器对 ts 语法的支持,可以跳过编译为 js 的步骤。

类型支持

因为 JS 类型系统存在“先天缺陷”,JS 代码中绝大部分错误都是类型错误(Uncaught TypeError),增加了Bug的查找和修改时间,严重影响开发效率。TS 是静态类型的编程语言,可以在编译期发现错误,方便Bug的查找和修改。

TS 支持所有 JS 的类型,但是 JS 不会检查类型的变化,但是 TS 会检查。

类型注解

在 TS 中,使用类型注解来为变量添加类型约束。

let age: number = 18	// 声明变量为数值类型

TS 的原始类型约束关键字有:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol

基础类型

数组类型严格将并不是 TS 新增的类型,但是 TS 的对象约束会根据具体类型的不同进行细分,使得数组也可以自成一类。

  • 数组类型
let number: number[] = [1, 3, 5]
let strings: Array<string> = ['a', 'b', 'c']
  • 联合类型
// 如果不确定变量的具体类型,或变量可能使用多个类型,则声明为联合类型
let a: number | string = 'a'	// 可以声明为数值或字符串
let arr: number | string[] = ['a', 'b', 'c']	// 可以声明为数值或字符串数组
// 如果数组中类型不止一种,则这样注解
let arr: (number | string)[] = [1, 'a', 3, 'b']		// 数组的元素可以是数值或字符串
  • 类型别名(自定义类型)
    当同一复杂类型被多次使用时,可以通过类型别名,简化该类型的使用
type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3, 'b']
  • 函数类型(参数类型、返回值类型)
// 单独指定参数和返回值的类型
function add(num1: number, num2: number): number {
	return num1 + num2
}
const add = (num1: number, num2: number): number => {
	return num1 + num2
}
// 同时指定参数、返回值类型
const add: (num1: number, num2: number) => number = (num1, num2) => {
	return num1 + num2
}
// 函数没有返回值,则返回值类型为 void
function greet(name: string): void {
	console.log('Hello', name)
}
// 可选参数
function mySlice(start?: number, end?: number): void {
	console.log('起始索引:', start, '结束索引:', end)
}
  • 对象类型(即对对象的属性和方法进行类型约束)
let person: { name: string; age: number; sayHi(): void } = {
	name: 'jack',
	age: 19,
	sayHi() {}
}
// 对象的属性和方法也可以是可选的
let config: { url: string; method?: string } = {...}
  • 接口类型(当一个对象类型被多次使用时,一般会使用接口来描述对象的类型,达到复用的目的)
interface IPerson {
	name: string
	age: number
	sayHi(): void
}
let person: IPerson = {
	name: 'jack',
	age: 19,
	sayHi() {}
}
// 接口类型和别名的区别在于,接口只能为对象指定类型,别名能为任意类型指定别名
// 如果两个接口之间有相同的属性或方法,可以将公共属性或方法抽离出来,通过继承来实现复用
interface Point2D { x: number; y: number }
interface Point3D extends Point2D { z: number }	// 继承了 Point2D 接口
  • 元组类型(另一种类型的数组,确定了元素的个数,以及特定索引元素的类型)
let posision: [number, number] = [39.5427, 116.2317]
  • 类型推论(在未明确指出类型的地方,使用类型推论机制,即省略类型注解)
// 类型推论场景1:声明变量时初始化
let age = 18
// 决定函数返回值时
function add(num1: number, num2: number) { return num1 + num2 }
  • 类型断言
    因为 ts 是静态语言,在编译时对于一些较为宽泛的对象并不确定其具体的属性,会造成无法访问某些特殊属性的错误。所以需要使用类型断言指定具体类型。
// 当有一个标签 
const aLink = document.getElementById('link')
// 变量 aLink 的类型是 HTMLElement,该是一个宽泛(不具体)的类型
// 包含所有标签公共的属性和方法,例如 id 属性,而不包含特有的属性,例如 href
// 如要操作特有的属性或方法,要进行类型断言
const aLink = document.getElementById('link') as HTMLAnchorElement
// as 后的类型必须为之前对象的子类
const aLink = <HTMLAnchorElement>document.getElementById('link')
  • 字面量类型
    在 ts 中,常量因为其值不能被改变,所以其类型为特殊的定义类型,即字面量类型。字面量类型可以是任意的 JS 字面量。通常字面量类型用在表示一组明确的可选值列表中。
const str = 'Hello TS'		// const 声明的变量为字面量类型,即 'Hello TS' 类型,而非 string 类型
function changeDirection(direction: 'up' | 'down' | 'left' | 'right') {		// 指定类型更加清晰明确
	console.log(direction)
}
  • 枚举类型
    枚举类型的功能类似于字面量类型+联合类型的组合功能,也可以表示一组明确的可选值
// 定义一组命名常量为枚举类型,它描述一个值,该值为这些命名常量中的一个
enum Direction { Up, Down, Left, Right }

function changeDirection(direction: Direction) {
	return direction
}
// 类似于对象,使用时可以使用 . 来访问枚举成员
changeDirection(Direction.Left)
// 枚举成员是有值的,默认为从数值 0 开始自增,即 Up, Down, Left, Right 值分别为 0, 1, 2, 3
// 如有需要,可以在定义时初始化枚举值,未明确定义初始值的以前一个初始值增加1
enum Direction { Up = 10, Down, Left, Right }	// 枚举值为 10, 11, 12, 13
// 也可以初始化每个枚举成员
enum Direction { Up = 10, Down = 16, Left = 22, Right = 32 }
// 枚举成员也可以定义为字符串枚举,字符串枚举的每个成员必须初始化值
enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }
  • any 类型(TS 不推荐使用 any 类型,因为这会丢失 TS 的类型保护的优势)
    any 类型为任意类型,会放弃 TS 的类型约束。
// 显式的声明 any 类型
let obj: any = { x: 0 }
// 隐式的声明 any 类型
let obj 	// 不提供类型注解也不初始化赋值
function fun(param) {		// 声明参数时不提供类型注解
	console.log(param)
}

typeof 运算符

在 JS 中,可以使用 typeof 运算符查看数据的类型,TS 也可以使用该运算符,并用于类型注解中

let p = { x: 1, y: 2 }
function formatPoint(point: typeof p) {}

高级类型

class 类

TS 全面支持 ES2015 中引入的 class ,并添加了类型注解和其他语法(比如可见性修饰符)。class 的基础使用 TS 和 JS 相同

class Person {}
const p = new Person()

根据 ts 中的类型推论,Person 类的实例对象 p 的类型是 Person。ts 中的 class 不仅提供了 class 语法功能,也作为一种类型存在。

ts 声明类需要进行类型注解或添加默认初始值

class Person {
	age: number
	gender = '男'		// 类型推论
}

构造函数和实例方法

类的构造函数需要指定类型注解,否则会被隐式推断为 any。构造函数不需要返回值类型

class Person {
	age: number
	gender: string

	constructor(age: number, gender: string) {
		this.age = age
		this.gender = gender
	}
}

类的实例方法的类型注解(参数和返回值)与函数用法相同

class Point {
	x = 10
	y = 10
	
	scale(n: number): void {
		this.x *= n
		this.y *= n
	}
}

继承

js 提供了类的继承 extends 可以继承父类,而 ts 提供另一种继承方法: implements,实现接口

// extends 继承父类
class Animal {
	move() { console.log('animal move') }
}
class Dog extends Animal {		// 继承父类
	bark() { console.log('wang!') }
}
const dog = new Dog()
// implements 实现接口
interface Singable {
	sing(): void
}
class Person implements Singable {		// 类中必须提供接口中所有方法和属性
	sing() {
		console.log('唱歌')
	}
}

ts 中接口class 类的区别在于,接口是对象的属性和方法的描述,是一种类型、约束。而类是对象的模板,类可以实例化为对象。

可见性

可以使用可见性修饰符来控制 class 的方法或属性对于 class 外的代码是否可见。可见性修饰符包括:

  • public(公有)
    公有的成员可以被任何地方访问,是默认的可见性
class Animal {
	public move() {
		console.log('Moving!')
	}
}
  • protected(受保护)
    受保护的成员仅对其声明所在类和子类中(非实例对象)可见
class Animal {
	protected move() { console.log('Moving!') }
}
class Dog extends Animal {
	bark() {
		console.log('wang!')
		this.move()		// 可以访问父类的 protected 方法
	}
}
  • private(私有)
    私有的成员仅对当前类可见,对实例对象以及子类都是不可见的
class Animal {
	private move() { console.log('Moving!') }
	walk() {
		this.move()
	}
}

只读

class 类常用的修饰符还有 readonly 修饰符,用来防止在构造函数之外对属性进行赋值

class Person {
	readonly age: number = 18
	constructor(age: number) {
		this.age = age
	}
}

类型兼容性

类型兼容性并不是一个具体的类型,而是 ts 中类型的一个特性。

现在常用的两种类型系统为

  • Structural Type System 结构化类型系统
  • Nominal Type System 标明类型系统

TS 采用的是结构化类型系统,也叫 duck typing(鸭子类型),类型检查关注的是值所具有的形状。也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。

class Point { x: number; y: number }
class Point2D { x: number; y: number }
const p: Point = new Point2D()			// 类型注解为 Point 类型,但是由于类型兼容性,可以使用 Point2D 类进行实例化

因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同。如果使用标明类型系统,如 C#, Java 等,因为是不同的类,类型无法兼容。

在实际使用的对象类型中,如果 y 的成员至少与 x 相同,则 x 兼容 y (成员多的可以赋值给少的),例如

class Point { x: number; y: number }
class Point3D { x: number; y: number; z: number }
const p: Point = new Point3D()

除了 class 之外,TS 的其他类型也存在相互兼容的情况:

  • 接口之间的兼容性类似于 class,且 class 和 interface 之间也可以兼容
  • 函数之间也有类型兼容性,不过较为赋值,需要考虑参数个数、参数类型、返回值类型
// 参数个数的影响:参数多的兼容参数少的(参数少的可以赋值给多的)
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1
let f2: F2 = f1
// 最常用的是数组的 forEach 方法,此方法的函数参数应该有3个参数,但实际使用中经常只使用第一个参数
// 即省略用不到的函数参数
arr.forEach(item => {})
arr.forEach((value, index, array) => {})
// 参数类型的影响:相同位置的参数类型要相同(原始类型)或兼容(对象类型)
type F1 = (a: number) => string
type F2 = (a: number) => string
let f1: F1
let f2: F2 = f1
// 如果函数参数是接口或 class,则和之前的接口或对象兼容性冲突
interface Point2D { x: number; y: number }
interface Point3D { x: number: y: number; z: number }
type F2 = (p: Point2D) => void
type F3 = (p: Point3D) => void
let f2: F2
let f3: F3 = f2		// f2 的成员少,则可以赋值给成员多的 f3,不可以反过来
// 返回值类型的影响:只关注返回值类型本身
// 返回值类型是原始类型,可以互相兼容
type F5 = () => string
type F6 = () => string
let f5: F5
let f6: F6 = f5		
// 返回值类型是对象或接口,成员多的可以赋值给成员少的
type F7 = () => { name: string }
type F8 = () => { name: string; age: number }
let f7: F7
let f8: F8
f7 = f8

交叉类型

交叉类型(&)功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)

interface Person { name: string }
interface Contact { phone: string }
type PersonDetail = Person & Contact	// PersonDetail 同时具备了 Person 和 Contact 的所有属性类型

&extends 的不同在于,同名属性之间,处理类型冲突的方式不同

interface A {
	fn: (value: number) => string
}
interface B extends A {
	fn: (value: string) => string	// 会报错,因为 extends 检查到类型不兼容
}
interface C {
	fn: (value: string) => string
}
type D = A & C		// 不会报错,交叉类型兼容两者,相当于 fn: (value: string | number) => string

泛型

泛型是在保证类型安全的前提下,让函数等与多种类型一起工作,从而实现复用,常用于函数、接口、class 中。例如:

// 函数返回参数数据本身,可以接收任意类型。如果不使用 any 类型,普通的类型只能实现单一类型,而使用泛型则可以实现
// 在函数名称后使用 <> 尖括号,并添加类型变量 Type。它是一种特殊类型的变量,它处理类型而不是值
function id<Type>(value: Type): Type { return value }
// 调用时再指定 Type 的具体类型
const num = id<number>(10)
const str = id<string>('a')
// 在实际使用中,通常会省略尖括号达到简化使用
const num1 = id(10)
const str1 = id('a')

泛型约束

因为默认情况下,泛型函数的变量类型 Type 可以代表多个类型,这导致无法访问任何属性,例如

function id<Type>(value: Type): Type {
	console.log(value.length)		// 会报错,因为类型 Type 不一定有 length 属性
	return value
}

此时可以添加泛型约束来收缩类型范围。泛型约束主要有两种方式:1.指定更加具体的类型,2.添加约束

// 指定更加具体的类型,收缩至数组类型
function id<Type>(value: Type[]): Type[] {
	console.log(value.length)
	return value
}
// 添加约束,满足某些条件要求
interface ILength { length: number }
function id<Type extends ILenght>(value Type): Type {
	console.log(value.length)
	return value
}

多个泛型

泛型的类型变量可以有多个,且类型变量之间也可以进行约束。例如

// 设置两个泛型,第二个泛型受到第一个泛型约束
function getProp<Type, Key extends keyof Type>(obj: Type, key: key) {
	return obj[key]
}

泛型接口

接口也可以使用泛型,不过在使用时,需要显式指定具体类型。

interface IdFunc<Type> {
	id: (value: Type) => Type
	ids: () => Type[]
}
let obj: IdFunc<number> = {
	id(value) { return value },
	ids() { return [1, 3, 5] }
}

实际上, JS 中的数组在 TS 中就是一个泛型接口

const strs = ['a', 'b', 'c']
const nums = [1, 2, 3]
// forEach 方法的参数就是根据数组元素不同而不同

泛型类

class 也可以使用泛型,例如 React 的组件的基类就是泛型类,不同的组件会有不同的成员类

interface IState { count: number }
interface IProps { maxLength: number }
class InputCount extends React.Component<IProps, IState> {
	state: IState = { count: 0 }
	render() { return <div>{this.props.maxLength}</div> }
}

创建泛型类类似于创建泛型接口

class GenericNumber<NumType> {
	defaultValue: NumType
	add: (x: NumType, y: NumType) => NumType
}
const myNum = new GenericNumber<number>()
myNum.defaultValue = 10

泛型工具

TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作,最常用的有以下几种:

  • Partial 用来构建一个类型,将 Type 的所有属性设置为可选
interface Props {
	id: string
	children: number[]
}
type PartialProps = Partial<Props>		// Props 的属性是必选的,经过 Partial 处理,新类型所有属性为可选
  • Readonly 用来构建一个类型,将 Type 的所有属性设置为只读
  • Pick 可以从Type中选择一组属性来构造新类型
interface Props {
	id: string
	title: string
	children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>
  • Record 用来构造一个对象类型,属性键为Keys,属性类型为Type,注意所有属性类型是相同的
// 创建一个对象,属性键为 a, b, c 类型均为 string[]
type RecordObj = Record<'a' | 'b' | 'c', string[]>
// 等同于
type RecordObj = {
	a: string[];
	b: string[];
	c: string[];
}

索引签名类型

绝大多数情况下,在使用对象前就能确定对象的结构,并为对象添加准确的类型。但是偶尔无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时可以使用索引签名类型

interface AnyObj {
	[key: string]: number
}

该例中使用 [key: string] 来约束该接口中允许出现的属性名称类型,这样可以出现任意多个符合约束的属性。 key 只是一个占位符,可以换成任意合法变量名称。例如 JS 中,对象 {} 的键是 string 类型的。

映射类型

映射类型可以基于旧类型创建新类型(对象类型),减少重复,提升开发效率。例如之前的泛型类型在内部是由映射类型实现的。

// 根据联合类型创建
type PropKeys = 'x' | 'y' | 'z'
type Type1 = { x: number; y: number; z: number}
// 使用映射类型实现:
type Type2 = { [key in PropKeys]: number }
// 根据对象类型创建
type Props = { a: number; b: string; c: boolean }
type Type3 = { [key in keyof Props]: number }
// 泛型工具类型 Partial 的实现
type Partial<Type> = {
	[P in keyof Type]?: T[P]
}

映射类型是基于索引签名类型的,所以该语法类似于索引签名类型,也使用 []。需注意的是,映射类型只能在类型别名中使用,不能在接口中使用。

索引查询(访问)类型

Partial 的实现中, T[P] 的语法在 TS 中叫做索引查询(访问)类型,可以用来查询属性的类型

type Props = { a: number; b: string; c: boolean }
type TypeA = Props['a']		// 即 number
type TypeB = Props['a' | 'b']	// number | string
type TypeC = Props[keyof Props]		// number | string | boolean

类型声明文件

TS 需要编译成 JS 代码执行,TS 提供了类型保护机制,为了将此机制延续到 JS,可以使用类型声明文件来为 JS 提供类型信息。

TS 中有两种文件类型,ts 文件和 d.ts 文件。ts 文件就是 ts 的可执行代码文件, d.ts 文件即类型声明文件,只做类型声明使用。

使用类型声明文件,在 d.ts 文件中使用 export 导出(也可以使用import/export实现模块化功能)。在需要使用共享类型的 ts 文件中,通过 import 导入即可(导入时 .d.ts 后缀可以省略)

// index.d.ts
type Props = { x: number; y: number }

export { Props }
// a.ts
import { Props } from './index'

let p1: Props = { x: 1, y:2 }

你可能感兴趣的:(typescript)