前言
TypeScript 是一种语言,它是 JavaScript 的超集。
安装 TypeScript:
npm install -g typescript
编译代码:
在命令行中,运行 TypeScript 编译器:
tsc greeter.ts
number 类型
表示变量类型为二进制、八进制、十进制、和十六进制。
let num1:number = 10 // 十进制
let num2:number = 0b110 //二进制
let num3:number = 0o765 //八进制
let num4:number = 0x12abc //十六进制
boolean 类型
boolean类型有两个值true和false。
let tag: boolean = false
let large: boolean = 30 > 40
string 类型
表示字符串值,如"Hello, world"。
let name:string = 'coder'
let age:number = 18
let person:string = `name: ${name}, age: ${age}`
array 类型
表示数组类型。
类型[]
let numbers: number[] = [1,2,3] // 数组元素全为number
let names: string[] = ['ts', 'jkl', 'rk'] // 数组元素全为string
let students: (string | number)[] = ['ts', 'jkl', 'rk', 11] // 数组元素为string或者number
泛型
let names: Array<string> = ['ts', 'jkl', 'rk'] // 数组元素全为string
let students: Array<string | number> = ['ts', 'jkl', 'rk', 11] // 数组元素为string或者number
object 类型
表示对象类型。
let person: object = {
name: 'ts',
age: 18
}
let person: { name:string, age: number} = {
name: 'ts',
age: 18
}
null 和 undefined 类型
JavaScript 有两个原始值用于表示不存在或未初始化的值:null 和 undefined。TypeScript 有两个对应的同名类型。
let u: undefined = undefined
let n: null = null
undefined和null是其它类型的子类型。
symbol 类型
JavaScript 中有一个原语用于通过函数创建全局唯一引用 Symbol()。
let person1 = Symbol("person")
let person2 = Symbol('person')
const student = {
person1: 'ts',
person2: 'jkl'
}
any 类型
TypeScript 也有一个特殊的类型,any,当你不希望某个特定的值导致类型检查错误时,你可以使用它。 当一个值是 type 为 any 时,您可以访问它的任何属性(这又将是type any),像函数一样调用它,将它分配给(或从)任何类型的值,或者几乎任何其他语法上的值合法的。
let title: any = 'hello'
title = 123
title = true
使用场景:
-
进行类型断言的时候:as any
-
不想给变量添加具体数据类型或者数据类型特别复杂
使用了any,就跟我们写javascript代码一样了。
unknown 类型
与any一样,所有类型都可以分配给 unknown。unknow 类型比 any 更加严格和安全。
let title: unknown = 'hello'
title = 123
title = true
unknown与any的区别:
-
unknown 类型只能赋值给 unknown 和 any,any 可以赋值给任意类型。
let msg: any = 'hello'
let student: string = msg // oK
---------------------------------------
let msg: unknown = 'hello'
let student: string = msg // 报错:不能将类型“unknown”分配给类型“string”
-
unknown 类型不能访问对象上的属性,any 可以,而且不存在的属性,any 也可以访问。
let obj: any = {
name: 'ts'
}
obj.age // ok
obj.name // ok
---------------------------------------
let obj: unknown = {
name: 'ts'
}
obj.age // 报错:对象的类型为 "unknown"
obj.name // 报错:对象的类型为 "unknown"
viod 类型
函数没有返回值,就可以用 viod 表示。一般会省略,不写。
function sum(num1, num2): void {
console.log(num1 + num2)
}
never 类型
never 表示永远不会发生值的类型,如 javascript 函数执行过程中的抛出异常,这时是不会有返回值的,我们往往用 never 类型表示。
function fn1(): never {
while (true) {
console.log('死循环')
}
}
function fn2(): never {
throw new Error('error')
}
tuple 类型
元组:多种元素的组合。明确数组里面每一项的类型。
let list: [string, number, boolean] = ['ts', 123, true]
function 类型
参数和返回值类型注解。
function sum(num1: number, num2: number): number {
return num1 + num2
}
sum(1, 2) // ok
函数可选参数
有时候,函数有些参数不是必须的,我们就可以将函数的参数设置为可选参数。可选参数只需在参数名后跟随一个 ? 即可。可以认为是参数类型和undefined的联合类型。
function sum(num1: number, num2?: number) {
console.log(num1) // 1
console.log(num2) // undefined
}
sum(1)
函数参数的默认值。
在 ES6 中,定义函数时给参数设默认值直接在参数后面使用等号连接默认值即可.
当为参数指定了默认参数时,TypeScript 会识别默认参数的类型;当调用函数时,如果给这个带默认值的参数传了别的类型的参数则会报错:
function sum(num1: number, num2: number = 10): number {
return num1 + num2
}
sum(1) // ok
sum(1, 'hello') // error 类型"string"的参数不能赋给类型"number"的参数
函数剩余参数。
在 JavaScript 中,如果定义一个函数,这个函数可以输入任意个数的参数,那么就无法在定义参数列表的时候挨个定义。在 ES6 发布之前,需要用 arguments 来获取参数列表。arguments 是一个类数组对象,它包含在函数调用时传入函数的所有实际参数,它还包含一个 length 属性,表示参数个数。
在 ES6 中,加入了…拓展运算符,它可以将一个函数或对象进行拆解。它还支持用在函数的参数列表中,用来处理任意数量的参数:
在 TypeScript 中可以为剩余参数指定类型:
function sum(num1: number, ...items: number[]): number[] {
return items
}
sum(1,2,3,4)
函数的重载。
JavaScript 作为一个动态语言是没有函数重载的,只能自己在函数体内通过判断参数的个数、类型来指定不同的处理逻辑。
function sum(num1: any, num2: any) {
if (typeof num1 === 'number' && typeof num2 === 'number') {
return num1 + num2
} else if (typeof num1 === 'string' && typeof num2 === 'string') {
return num1.length + num2.length
}
}
sum(1, 4)
sum('hello', 'world')
可以看到传入的参数类型不同,处理逻辑是不同的。
那有没有一种办法可以更精确地描述参数与返回值类型约束关系的函数类型呢? 这就是函数重载。
函数重载就是同一个函数,通过参数个数或者类型不同来指定多个函数类型定义。
function sum(num1: number, num2: number): number
function sum(num1: string, num2: string): string
function sum(num1: any, num2: any) {
return num1 + num2
}
sum(1, 2)
sum('hello', 'world')
在调用 sum 函数时,TypeScript 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义。
接口定义函数。
参数定义:函数返回值的类型。
interface IFn {
(num: number, num2: number): number
}
const sum: IFn = (num1, num2): number => num1 + num2
sum(1, 2) // 3
类型别名定义函数。
type Fn = (num: number, num2: number) => number
const sum: Fn = (num1, num2) => num1 + num2
sum(1, 3) // 4
注意,这里的=>与 ES6 中箭头函数的=>不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。
类型别名
我们一直通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但通常希望多次使用同一个类型并用一个名称引用它。
类型别名就是这样 -任何类型的名称。类型别名的语法是:
type Point = {
x: number;
y: number;
};
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
可以使用类型别名来为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类型:
type ID = number | string;
交叉类型
交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。例如,Person & Serializable & Loggable 同时是 Person 和 Serializable 和 Loggable。就是说这个类型的对象同时拥有了这三种类型的成员。
type Dog = {
name: string
}
type Cat = {
age: number
}
type Animal = Dog & Cat
let mouse: Animal = {
name: 'thy',
age: 18
}
联合类型
联合类型与交叉类型很有关联,但是使用上却完全不同。联合类型表示一个值可以是几种类型之一。我们用竖线 | 分隔每个类型。
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
printId(101) // OK
printId("202") // OK
printId({ myID: 22342 }) // Error
printId(true) // Error
类型断言
在某些情况下,我们会比 Typescript 更清楚一个数据的类型。这种时候,我们就可以使用断言。类型断言用于手动指定一个值的类型。
语法:as 或者 <>。在tsx中必须使用前者。
将一个联合类型断言为其中一个类型
interface Cat {
name: string
run(): void
}
interface Fish {
name: string
swim(): void
}
function isFish(animal: Cat | Fish): boolean {
return typeof (animal as Fish).swim === 'function'
}
将变量 animal 断言为 Fish 类型,那么访问其 swim 属性,就可以通过编译器的检查。
类型断言相当于欺骗编译器,编译的时候不报错,不代表运行的时候不报错。
const tom:Cat = {
name:"cat",
run(){
console.log("running")
}
}
function swim(animal:Cat|Fish){
(animal as Fish).swim()
}
swim(tom)
编译结果:
var tom = {
name: "cat",
run: function () {
console.log("running")
}
}
function swim(animal) {
animal.swim()
}
swim(tom) // TypeError: animal.swim is not a function
使用any临时断言
window.a = "hello world" // Property 'a' does not exist on type 'Window & typeof globalThis'
只是想给 window 添加一个属性,但因为 window 上并没有 a 属性,所以报错。此时,就需要将 window 断言为 any:
(window as any).a = "hello world"
不能滥用 as any,也不能完全否定它的作用,需要在类型的严格性和开发的便利性之间掌握平衡。
Class类
传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。从 ECMAScript 2015,也就是 ECMAScript 6开始,JavaScript 程序员将能够使用基于类的面向对象的方式。
基本使用
class Person {
name: string
age: number = 18
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
const person = new Person('ts', 18)
注意:如果定义了变量不使用,编译会报错,需要给变量一个默认值。
Property 'age' has no initializer and is not definitely assigned in the constructor
继承
在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
class Person {
name: string = ''
age: number = 18
sleep() {
console.log('sleep')
}
}
class Teacher extends Person {
title: string = 'teacher'
skill() {
console.log('teaching')
}
}
const ts = new Teacher()
console.log(ts.age) // 18
ts.sleep() //sleep
console.log(ts.title) // teacher
ts.skill() // teaching
这个例子展示了最基本的继承:类从基类中继承了属性和方法。这里 Teacher 是一个派生类,它派生自 Person 基类,通过 extends 关键字。派生类通常被称作子类,基类通常被称作超类。
因为 Teacher 继承了 Person 的功能,因此我们可以创建一个 Teacher 的实例,它能够拥有 sleep()和age。
下面我们来看个更加复杂的例子。
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
skill() {
console.log('eating')
}
}
class Teacher extends Person {
title: string = 'teacher'
constructor(name: string, age: number) {
super(name, age)
}
skill() {
super.skill()
console.log('teaching')
}
}
const ts = new Teacher('ts', 18)
console.log(ts.name) // ts
console.log(ts.age) // 18
console.log(ts.title) // teacher
ts.skill() // eating teaching
与前一个例子的不同点是,派生类 Teacher 包含了一个构造函数,它必须调用 super(),它会执行基类 Person 的构造函数。而且,在构造函数里访问 this 的属性之前,我们一定要调用 super()。 这个是TypeScript 强制执行的一条重要规则。
这个例子演示了如何在子类里可以重写父类的方法。Person 类和 Teacher 类都创建了 skill 方法,Teacher 重写了从 Person 继承来的 skill 方法,使得 skill 方法根据不同的类而具有不同的功能。
多肽
class Animal {
eating() {
console.log('meat')
}
}
class Dog extends Animal {
eating() {
console.log('bone')
}
}
class Cat extends Animal {
eating() {
console.log('fish')
}
}
function eatingList(animals: Animal[]) {
animals.forEach(item => {
item.eating()
})
}
eatingList([new Dog(), new Cat()]) // bone fish
实现多肽所必须的几个条件:
1.多肽是发生在父类和子类之间的,单一的一个类不可能实现多肽。
2.父类和子类必须发生重写函数。
3.在使用多肽特性时,必须定义一个父类的指针或者引用。(即父类引用指向子类对象)
类的多肽让我们的代码能更加具有通用性,更加灵活。
修饰符
类一共有三种修饰符:public、private、protected。
1. public
在 TypeScript 里,成员都默认为 public ,我们可以自由的访问程序里定义的成员。
class Person {
public name: string
public constructor(name: string) {
this.name = name
}
public skill() {
console.log('eating')
}
}
2. private
当成员被标记成 private时,它就不能在声明它的类的外部访问。
class Person {
private name: string
constructor(name: string) {
this.name = name
}
getName() {
return this.name
}
}
new Person('Cat').name // error:属性“name”为私有属性,只能在类“Animal”中访问。
3. protected
protected 修饰符与 private 修饰符的行为很相似,但有一点不同, protected 成员在子类中仍然可以访问。
class Person {
protected name: string
constructor(name: string) {
this.name = name
}
}
class Teacher extends Person {
constructor(name: string) {
super(name)
}
getName() {
console.log(this.name)
}
}
new Teacher('Cat').name // error: 属性“name”受保护,只能在类“Person”及其子类中访问。
readonly
readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Person {
readonly name: string
constructor(name: string) {
this.name = name
}
}
const person = new Person('jkl')
person.name = 'ts' // error: 无法分配到 "name" ,因为它是只读属性。
存取器
TypeScript 支持通过 getters/setters 来截取对对象成员的访问。它能帮助你有效的控制对对象成员的访问。
class Person {
private _name
constructor(name: string) {
this._name = name
}
get name() {
return this._name
}
set name(name: string) {
this._name = name
}
}
const person = new Person('jkl')
person.name = 'ts'
console.log(person.name) // ts
静态属性
静态属性或者方法只能通过类去访问,不能通过 this 和子类访问。静态属性存在于类本身上面而不是类的实例上。
class Person {
static time: string = '12: 00: 00'
constructor() {
// this.time = time // error: 属性“time”在类型“Person”上不存在。
}
static eating() {
console.log('eating')
}
}
const person = new Person()
// console.log(person.time) // error: 属性“time”在类型“Person”上不存在。
console.log(Person.time) // 12: 00: 00
Person.eating() // eating
抽象类
抽象类做为其它派生类的基类使用。它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Person {
constructor() {}
skill() {}
// 抽象类中的抽象方法不能具有实现
abstract eating(): void
}
class teacher extends Person {
// 抽象类中的抽象方法eating必须在派生类中实现
eating() {
console.log('eating')
}
}
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。抽象方法的语法与接口方法相似。两者都是定义方法签名但不包含方法体。然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。
把类当做接口使用
因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Person {
name: string
age: number
eating() {}
}
let ts: Person = {
name: 'shy',
age: 18,
eating() {
console.log('eating')
}
}
接口类型
接口(interface)主要用来声明对象类型,interface来定义一种约束,检查对象的结构是否满足约束。
基本使用
interface IPerson {
name: string
eating(): void
}
let ts: IPerson = {
name: 'thy',
eating() {
console.log('eating')
}
}
IPerson 接口就好比一个名字,用来描述上面例子里的要求。它代表了有一个 name 属性且类型为 string 、一个eating方法的对象。我们只会去关注值的外形。只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
可选属性
接口里的属性不全都是必需的。有些是只在某些条件下存在,或者根本不存在。
interface IPerson {
name?: string
age: number
eating(): void
}
let ts: IPerson = {
age: 18,
eating() {
console.log('eating')
}
}
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。
可选属性带来的好处:
-
对可能存在的属性进行预定义。 -
可以捕获引用了不存在的属性时的错误。
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly 来指定只读属性。
interface IPerson {
readonly name: string
age: number
}
let ts: IPerson = {
name: 'thy',
age: 18
}
ts.name = 'jkl' // error: 无法分配到 "name" ,因为它是只读属性
索引类型
我们可以描述那些能够"通过索引得到"的类型,比如 a[10]或 ageMap["daniel"]。可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
interface IPerson {
[index: number]: string
}
let students: IPerson = {
0: 'ts',
1: 'jkl',
2: 'rk'
}
interface IAge {
[name: string]: number
}
let person: IAge = {
ts: 2000,
jkl: 2001,
rk: 1997
}
console.log(students[0]) // ts
console.log(person['ts']) // 2000
函数类型
接口也可以描述函数类型。为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface IFn {
(num1: number, num2: number): number
}
let sum: IFn = (num1, num2) => num1 + num2
console.log(sum(10, 20)) // 30
接口继承
和类一样,接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface IStudent {
name: string
}
interface ITeacher {
age: number
}
interface IChild extends IStudent, ITeacher {
sex: string
}
let ts: IChild = {
name: 'shy',
age: 18,
sex: 'male'
}
接口实现
与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。
interface IStudent {
name: string
}
interface ITeacher {
age: number
}
class Person implements IStudent, ITeacher {
name = 'ts'
age = 18
}
interface与Type的区别
类型别名和接口非常相似,在很多情况下可以在它们之间自由选择。主要区别在于接口可以重复声明去添加新属性而类型别名不行。
接口通过继承去扩展。
interface IStudent {
name: string
}
interface ITeacher extends IStudent {
age: number
}
let ts: ITeacher = {
age: 18,
name: 'shy'
}
类型别名通过交叉扩展类型。
type Animal = {
name: string
}
type Bear = Animal & {
age: number
}
let ts: Bear = {
name: 'shy',
age: 18
}
接口可以通过重复声明去扩展,类型别名重复声明会报错。
// 接口重复声明
interface IPerson {
name: string
}
interface IPerson {
age: number
}
let person: IPerson = {
name: 'ts',
age: 18
}
// 类型别名重复声明
type Person = {
name: string
}
type Person = {
age: number
}
// error: 标识符“Person”重复。
类型别名可能不参与声明合并,但接口可以。
接口只能用于声明对象的形状,不能声明基本类型。
在大多数情况下,我们可以根据个人喜好进行选择。官方推荐考虑接口优先。
枚举类型
使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript 支持数字的和基于字符串的枚举。
数字枚举
enum Direction {
Up = 1,
Down,
Left,
Right
}
我们定义了一个数字枚举,Up 使用初始化为1。其余的成员会从1开始自动增长。换句话说,Direction.Up的值为1,Down为2,Left为3,Right为4。
如果我们没有设置初始化值,默认从0开始。
字符串枚举
字符串枚举里,每个成员都必须用字符串字面量。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。换句话说,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的。它并不能表达有用的信息,字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。
反向映射
除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了反向映射,从枚举值到枚举名字。例如,在下面的例子中:
enum Enum {
Male
}
let a = Enum.Male;
let nameOfA = Enum[a]; // "Male"
泛型类型
泛型是 Typescript 中使用最多的类型之一,它用来表示当前的数据类型,同时也能支持未来的数据类型,可以理解成一种动态的数据类型,类似于函数里面的形参,数据类型由外界的调用者决定,即类型参数化。
基本使用
这里,我们实现一个函数,传入一个number类型参数,返回这个参数。
function sum(num: number): number {
return num
}
sum(10) // 10
我们用泛型进行改写:
function sum<T>(num: T): T {
return num
}
sum(10) // 10
我们用泛型T来表示动态的类型,由调用sum函数时,参数的类型决定。一般用T表示泛型类型,用别的大写字母也可以。
也可以存在多个泛型类型:
function sum<T,U>(num1: T, NUM2: U){
}
sum(10, 'HELLO') // 10
泛型接口
接口也可以配合泛型,更加灵活。
interface IPerson<T, U> {
name: T
age: U
}
let person: IPerson<string, number> = {
name: 'ts',
age: 18
}
泛型类
泛型类看上去与泛型接口差不多。泛型类使用( <>)括起泛型类型,跟在类名后面。
class Person<T, U> {
name: T
age: U
constructor(name: T, age: U) {
this.name = name
this.age = age
}
}
let person = new Person('ts', 18)
泛型约束
我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。比如,我们想访问 arg 的 length 属性,但是编译器并不能证明每种类型都有length属性,这样就会报错。
使用泛型约束即对参数的类型进行约束,只接受有length属性的参数,这样就不会报错了。
interface ILength {
length: number
}
function getLength<T extends ILength>(arg: T): number {
return arg.length
}
console.log(getLength('hello')) // 5
console.log(getLength(true)) // error: 类型“boolean”的参数不能赋给类型“ILength”的参数
使用keyof约束对象
在泛型约束的基础上,也可以通过keyof用对象的key值进行约束。
function fn<T, U extends keyof T>(obj: T, key: U) {
return obj[key]
}
let o = { name: 'ts', age: 18 }
fn(o, 'name')
fn(o, 'sex') // error: 类型“"sex"”的参数不能赋给类型“"name" | "age"”的参数。