TS 和 JS 相比,增加的功能包括:
并且有些功能从 ECMA2015 反向移植过来:
因为 TS 是 JS 的超集,所以这里只研究与 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 的原始类型约束关键字有:
数组类型严格将并不是 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 }
// 当有一个标签
const aLink = document.getElementById('link')
// 变量 aLink 的类型是 HTMLElement,该是一个宽泛(不具体)的类型
// 包含所有标签公共的属性和方法,例如 id 属性,而不包含特有的属性,例如 href
// 如要操作特有的属性或方法,要进行类型断言
const aLink = document.getElementById('link') as HTMLAnchorElement
// as 后的类型必须为之前对象的子类
const aLink = <HTMLAnchorElement>document.getElementById('link')
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 类型
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) {}
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 外的代码是否可见。可见性修饰符包括:
class Animal {
public move() {
console.log('Moving!')
}
}
class Animal {
protected move() { console.log('Moving!') }
}
class Dog extends Animal {
bark() {
console.log('wang!')
this.move() // 可以访问父类的 protected 方法
}
}
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 中类型的一个特性。
现在常用的两种类型系统为
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 的其他类型也存在相互兼容的情况:
// 参数个数的影响:参数多的兼容参数少的(参数少的可以赋值给多的)
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 }