之前在自研框架的时候,选用TS开发,使用webpack打包,但是最后由于没有系统研究过TS的开发流程和具体内容,还是以JS的思维去写,感觉不到TS的强大。因此本篇系统学习TS。
什么是TypeScript?
- 类型系统
- 适用于任何规模
类型系统
「类型」是其最核心的特性,JavaScript 是一门弱类型的编程语言。TypeScript 是静态类型,动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。JavaScript 是一门解释型语言,没有编译阶段,所以它是动态类型。
TypeScript 强大的「类型推论」也可以自动推论出它是一个什么类型,从而判断你代码是否正确使用。
我们都听过强语言、弱语言。那么有没有听过强类型和弱类型呢?
类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。,TypeScript 是弱类型。
第一个案例Hello TS
function sayHello(person: string) {
if (typeof person === 'string') {
return 'Hello, ' + person
} else {
throw new Error('person is not a string')
}
}
let user = 'TS';
console.log(sayHello(user))
在 TypeScript 中,我们使用 : 指定变量的类型,: 的前后有没有空格都可以。
原始数据类型
JavaScript 的类型分为两种:
- 原始数据类型(Primitive data types)
- 对象类型(Object types)
原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。本篇只介绍一些特殊的情况。
空值
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数。
声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null。
function alertName(): void {
alert('My name is Tom')
}
Null 和 Undefined
与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量可以赋值给 number 类型的变量,而 void 类型的变量不能赋值给 number 类型的变量。
let num: number = undefined // 严格模式下,会报错
任意值
任意值(Any)用来表示允许赋值为任意类型。声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。也就是不限制类型。
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型。
但是慎用,everthing is any,ts 基本就是 js,检查变量的时候也不能快速知道类型。
let myFavoriteNumber: string = 'seven'
myFavoriteNumber = 7
类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
let myFavoriteNumber = 'seven'
myFavoriteNumber = 7
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。联合类型使用 | 分隔每个类型。当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。
let myFavoriteNumber: string | number
myFavoriteNumber = 'seven'
myFavoriteNumber = 7
接口
此接口非彼接口,而是对应后端开发的接口Interfaces。
类似于后端开发,需要定义数据库表的实体类,亦或者,对某一种功能通过接口去抽象方法函数和返回值,通过实现接口来完成工作。
interface Person {
name: string
age: number
}
let tom: Person = {
name: 'Tom',
age: 25
}
可选属性
有时我们希望不要完全匹配一个形状,那么可以用可选属性。
interface Person {
name: string
age?: number
}
let tom: Person = {
name: 'Tom'
}
任意属性
使用 [propName: string] 定义了任意属性取 string 类型的值。
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型,或者可以修改任意属性的类型为 any。
interface Person {
name: string
age?: number
[propName: string]: string
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
}
// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.
只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性。注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。
interface Person {
readonly id: number
name: string
age?: number
[propName: string]: any
}
let tom: Person = {
id: 114514,
name: 'Tom',
gender: 'male'
};
tom.id = 1919810;
// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
数组的类型
在 TypeScript 中,数组类型有多种定义方式。
「类型 + 方括号」
let fibonacci: number[] = [1, 1, 2, 3, 5]
数组泛型
这和java感觉很像,看来我学了一点jvav也是有点用的。
let fibonacci: Array<number> = [1, 1, 2, 3, 5]
接口表示数组
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5]
类数组
注意例如arguments,不能用普通的数组的方式来描述,而应该用接口。
function sum() {
let args: {
[index: number]: number
length: number
callee: Function
} = arguments
}
事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等。
其中 IArguments 是 TypeScript 中定义好了的类型。
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
任意类型数组
一个比较常见的做法是,用 any 表示数组中允许出现任意类型。
let list: any[] = ['114514', 233, { name: 'xiaoxi' }]
函数的类型
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)。
函数是一等公民
当我们说函数是“一等公民”的时候,我们实际上说的是它们和其他对象都一样,没什么特殊的,你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量...等等。
const hi = name => `Hi ${name}`
const greeting = name => hi(name)
hi("jonas"); // "Hi jonas"
greeting("times"); // "Hi times"
这种代码主要是体现在ajax回调上。
httpGet('/post/2', json => renderPost(json))
// 可以传递 err 参数。
httpGet('/post/2', (json, err) => renderPost(json, err))
虽说添加一些没有实际用处的间接层实现起来很容易,但这样做除了徒增代码量,提高维护和检索代码的成本外,没有任何用处。
优化的写法:
// renderPost 将会在 httpGet 中调用,想要多少参数都行
httpGet('/post/2', renderPost)
在命名的时候,我们特别容易把自己限定在特定的数据上(本例中是 articles)。这种现象很常见,也是重复造轮子的一大原因。
// 只针对当前的博客
const validArticles = articles =>
articles.filter(article => article !== null && article !== undefined),
// 对未来的项目更友好
const compact = xs => xs.filter(x => x !== null && x !== undefined)
TypeScript函数声明
function sum(x: number, y: number): number {
return x + y
}
function sum(x: number, y: number): number {
return x + y
}
sum(1, 2, 3)
// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
函数表达式
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
用接口定义函数的形状
我们也可以使用接口的方式来定义一个函数需要符合的形状。
interface SearchFunc {
(source: string, subString: string): boolean
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1
}
可选参数
可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了。
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName
} else {
return firstName
}
}
let tomcat = buildName('Tom', 'Cat')
let tom = buildName('Tom')
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数。此时就不受「可选参数必须接在必需参数后面」的限制。
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName
}
let tomcat = buildName('Tom', 'Cat')
let tom = buildName('Tom')
剩余参数
ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数。事实上,items 是一个数组。所以我们可以用数组的类型来定义它。
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item)
});
}
let a = []
push(a, 1, 2, 3)
重载
我们可以使用重载定义多个 reverse 的函数类型。这样能够精确的表达,输入为数字的时候,输出也应该为数字。
function reverse(x: number): number
function reverse(x: string): string
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''))
} else if (typeof x === 'string') {
return x.split('').reverse().join('')
}
}
类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法。
用类型断言,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiError。
interface ApiError extends Error {
code: number
}
interface HttpError extends Error {
statusCode: number
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true
}
return false
}
我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'
此时我们可以使用 as any 临时将 window 断言为 any 类型。
(window as any).foo = 1
总结:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
声明文件
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
新语法
- declare var 声明全局变量
- declare function 声明全局方法
- declare class 声明全局类
- declare enum 声明全局枚举类型
- declare namespace 声明(含有子属性的)全局对象
- interface 和 type 声明全局类型
- export 导出变量
- export namespace 导出(含有子属性的)对象
- export default ES6 默认导出
- export = commonjs 导出模块
- export as namespace UMD 库声明全局变量
- declare global 扩展全局变量
- declare module 扩展模块
- ///
三斜线指令
本文参考于 xcatliu 的 TypeScript入门教程