类型
基础类型
基础类型过一下
| 类型 | 说明 |
|---|---|
| string | 字符串 |
| number | 数值 |
| boolean | 布尔 |
| null | 空 |
| undefined | 未定义 |
| Symbol | ES6新增的基础类型,表示独一无二的值,可以用来定义私有变量 |
| BigInt | JS第七种基础数据类型,更大精度的整数 |
| void | 无类型,一般用作表示函数的无返回值 |
基础类型使用方法:
let str: string = 'hello'
let num: number = 123
...
数组、元组
数组:
// 定义了一个字符串数组
const arr1: string[]= ['a', 'b']
// 第二种定义数组方式,不过一般使用上面那种
const arr2: Array<string> = ['a', 'b']
元组:
元组就是一个固定长度,且指定了每个元素的类型,比如可以用来定义csv数据格式。
// 第一位为string,第二位为number的元组
const tuple1: [string, number] = ['a', 1]
tuple1.push(1)
// 尽管上面push了一个元素,当越界访问元组仍然会报错
tuple1[2] // error
// 元组部分固定
const tuple2: [...string[], number] = ['a', 'b', 1]
const tuple3: [string, ...number[]] = ['a', 1, 2]
// 元组标签,没有实际作用,编辑器提示会携带,看起来更直观,相当于注释的效果
const tuple4: [str: string, num: number] = ['a', 1]
对象
// 基础对象类型
const obj1: object = { name: 'ly', age: 18 }
// 限制对象属性,只能有name和age,且限制了类型
const obj2: {
name: string,
age: number
} = { name: 'ly', age: 18 }
// 部分固定,可以随意写额外的元素
const obj3: {
name: string,
age: number,
[key: string]: any
} = { name: 'ly', age: 18, job: 'web' }
可选类型
const obj: {
name: string
age?: number // ?: 表示可选类型,即可以为undefined
} = {
name: 'ly'
}
函数
基本用法
// fn函数,接收一个string类型参数,返回值也是string
function fn(name: string): string {
return 'hello ' + name
}
函数参数(可选、只读,默认值)
function fn1(userInfo: {
name: string
readonly age: number
job?: string
}, log: boolean = false): void {
userInfo.name.slice(0, 1) // OK
userInfo.age = 18 // error,因为age为只读属性
userInfo.job.slice(0, 1) // error,因为job是可选,类型可能为undefined
if (log) {
console.log(userInfo)
}
}
// error,当给参数指定了默认值就不要设置为可选类型
function fn2(log?: boolean = false) {}
枚举 enum
// 基础用法,Up、Down、Left、Right依次为0 1 2 3
enum Direction1 {
Up,
Down,
Left,
Right,
}
Direction1.Up // 值为0
// 编译成js
var Direction1;
(function (Direction1) {
Direction1[Direction1["Up"] = 0] = "Up";
Direction1[Direction1["Down"] = 1] = "Down";
Direction1[Direction1["Left"] = 2] = "Left";
Direction1[Direction1["Right"] = 3] = "Right";
})(Direction1 || (Direction1 = {}));
// 可以发现其实就是一个对象映射,同时也做了反向映射
Direction1[0] // 值为Up
// Up、Down、Left、Right依次为1 2 3 4
enum Direction2 {
Up = 1,
Down,
Left,
Right,
}
// 字符串枚举,指定了值,将不再是自动生成的0 1 2 3
enum Direction2 {
Up = 'u',
Down = 'd',
Left = 'l',
Right = 'r',
}
// 异构枚举,前面三个依次为0 1 2,最后一个为指定的r,一般没啥用
enum Direction3 {
Up,
Down,
Left,
Right = 'r',
}
// 注意,字符串枚举无法直接赋值
const direction1: Direction2 = 'u' // error
const direction2: Direction2 = Direction2.Up // OK
// 常量枚举,enum前面带有const,不会编译出js代码
const enum Direction4 {
Up = 'u',
Down = 'd',
Left = 'l',
Right = 'r',
}
never
never代表永不存在的值类型,可用于定义函数报错、无返回,限制类型传入
// age为never,限制了永远不能存在age属性
const obj: {
name: string
age: never
} = {
name: 'ly',
age: 18 // error
}
// 死循环,永远无返回
function fn1(): never {
while(true){}
}
// 报错
function fn2(): never {
throw new Error('报错了')
}
// 用途示例,限制参数必传,如果name未传入,会触发赋值error,则抛出错误
// 准确来说,这个用途是位于js层面的,和ts关系不太大,本例仅做示范
function fn3(name = error('name')) {
}
function error(key: string): never {
throw new Error(`${key}为必传`)
}
any和unknown
- any,代表任意类型,相当于写JS代码,可以被赋予任何类型,非特殊情况不建议使用
// 后续可以任意修改类型
let val: any = '23'
val = 1
val = true
// 赋值给其他类型也不报错
let str: string = val
- unknown,代表不确定类型,和any相反,不能赋值给其他非unknown的类型,被认为是类型安全的,推荐使用,使用前都需要进行断言,断言后面会讲
let val1: unknown = 'a'
// 因为是unknow,ts不确定val1是string,所以认为不存在slice方法
val1.slice(1,2) // error
let val2: string = val // error
let val2: string = val as string // 需要断言为string后才能赋值给string类型
联合类型
多个类型满足其一即可,用 | 连接多个类型
let val1: string | number = 'hello'
val1 = 123 // OK
let val2: { name: string } | { age: number } = {
name: 'ly',
age: 18
} // 注意这里不会报错,但后续访问值会报错
val2.name // error,不确定是否存在name,因为类型2不存在name
val2.age // error,不确定是否存在age,因为类型1不存在age
交叉类型
多个类型都需要满足,用 & 连接多个类型
let val1: { name: string } & { age: number } = {
name: 'ly',
age: 18
}
val1.name // OK
val1.age // OK
// 如果是多个不可能相交的类型,则会得出never类型
let val2: string & number = 'hello' // error,交叉类型结果是never
字面量类型
意思是把字面量的值当做类型
// val的值只能是hello,看起来除了作为常量没什么其他用处,不过搭配联合类型挺有用的
let val1: 'hello' = 'hello'
// val2可以是1或2或3,比枚举用起来方便一点
let val2: 1 | 2 | 3 = 1
类型推断
类型推断是指没有人为指定类型,ts为你推断出来的类型,仅在简单场景下有效
// 以下两种写法效果一致,因为ts自动推断出val2是string类型
const val1: string = 'hello'
const val2 = 'hello'
// 返回值将被推断为string
function fn() {
return 'hello'
}
fn().slice(1, 2) // OK
// 属性推断
const obj1 = { name: 'ly', age: 18 }
obj.name // OK
obj.age // OK
// 反向推断
const obj2 = { name: 'ly', age: 18 }
// typeof获取obj2的类型:{ name: string, age: number }
const obj3: typeof obj2 = {
name: 'ly',
age: 18
}
类型断言
类型断言是指人为告诉ts这个变量的类型是什么
// 语法1
const val1: unknown = 'hello'
// error,unknown类型不能赋值给string类型,尽管它的值就是stirng类型,因为ts并不知道
const val2: string = val1
const val2: string = val1 as string // OK
// 语法2,这种写法会和tsx语法冲突
const val3: string = <string>val1
非空断言
function fn1(name: string | null) {
// name可能为null,所以会报错
return name.slice(0, 1) // error
}
function fn2(name: string | null) {
// name后面加上了一个叹号,告诉ts name不可能为null或undefined
return name!.slice(0, 1) // OK
}
确定赋值断言
// 确定赋值断言,告诉ts这个变量我一定会赋值
let msg1: string
if (flag) {
msg1 = 'hello'
}
msg1.slice(0, 1) // error,因为msg赋值是在条件判断中,不确定是否被赋值了
let msg2!: string // !: string 表示确定会被赋值
if (flag) {
msg2 = 'hello'
}
msg2.slice(0, 1) // OK
自定义类型(类型别名) type
自定义类型,就是给指定类型起个别名
// 基础用法
type StrOrNum = string | number
let val: StrOrNum = 'hello'
val = 123 // OK
// 继承
type Person = {
name: string
age: number
}
type Man = Person & {
job: string
}
// 定义一个函数
type Log = (value: any) => void
// 递归引用,定义树形数据格式
type TreeItem = {
label: string
value: number
children: TreeItem[]
}
接口 interface
接口和自定义类型相似,定义一些类型的集合
// 基础用法
interface User1 {
name: string
age: number
}
// 只读类型,上面的自定义类型也可!
interface User2 {
name: string
readonly age: number
}
const obj: User2 = {
name: 'ly',
age: 18
}
obj.name = 'yl' // OK
obj.age = 19 // error
// 接口继承
// Coder接口继承User1,代表拥有User1的类型
interface Coder extends User1 {
job: string
}
// 继承多个
interface Chinese {
lang: 'zh'
}
interface Coder extends User1, Chinese {
job: string
}
// 定义一个函数,User3代表一个函数类型
interface User3 {
(str: string): void
}
// 包含函数,User4是一个对象类型,里面拥有一个函数类型
interface User4 {
speak: (msg: string) => void
name: string
}
// 也可以递归引用
interface TreeItem {
label: string
value: number
children: TreeItem[]
}
// 混合类型,定义一个函数,函数上存在其他属性,如JQuery
interface JQuery {
(selector: string): JQuery
html: (html: string) => string
css: (css: string) => string
}
重复定义
interface可以重复定义,这一点和type不同;
interface User {
name: string
}
interface User {
age: number
}
// 最终的User取两个定义的合集
interface User {
age: number // error, 不能重复定义属性
}
vue3利用这个特性,实现了向外部暴露扩展全局属性
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$formateDate(date: Date): string {}
}
}
// 其原理就是vue内部定义了一个空的ComponentCustomProperties interface可以重复定义,然后把这个interface继承到了ComponentPublicInstance上,然后就实现了全局属性声明扩展
// 贴一下vue内部的相关代码
export declare type ComponentPublicInstance<P = {}, // props type extracted from props option
B = {}, // raw bindings returned from setup()
D = {}, // return from data()
C extends ComputedOptions = {}, M extends MethodOptions = {}, E extends EmitsOptions = {}, PublicProps = P, Defaults = {}, MakeDefaultsOptional extends boolean = false, Options = ComponentOptionsBase<any, any, any, any, any, any, any, any, any>> = {
$: ComponentInternalInstance;
$data: D;
$props: MakeDefaultsOptional extends true ? Partial<Defaults> & Omit<P & PublicProps, keyof Defaults> : P & PublicProps;
$attrs: Data;
$refs: Data;
$slots: Slots;
$root: ComponentPublicInstance | null;
$parent: ComponentPublicInstance | null;
$emit: EmitFn<E>;
$el: any;
$options: Options & MergedComponentOptionsOverride;
$forceUpdate: () => void;
$nextTick: typeof nextTick;
$watch(source: string | Function, cb: Function, options?: WatchOptions): WatchStopHandle;
} & P & ShallowUnwrapRef<B> & UnwrapNestedRefs<D> & ExtractComputedReturns<C> & M & ComponentCustomProperties;
// 最后面通过&符号继承了ComponentCustomProperties
// 这里定了一个空的interface
export declare interface ComponentCustomProperties {
}
官方建议,在实际开发中,能用interface实现的就用interface,实现不了的再用type;
interface给出的类型提示就是interface的名字,而type因为仅仅只是一个类型的引用,所以类型提示就是具体的类型。
类型守卫
当需要变量为某种类型时,才能进行某些操作,这时就需要用到类型守卫
// typeof做类型守卫
function fn1(code: string | number) {
if(typeof code === 'string') {
// trim是string类型才有的方法,如果没有通过typeof做类型守卫,那么就会报错
return code.trim() // OK
}
return code
}
// in操作符做类型守卫
function fn2(obj: { name: string } | { age: number }) {
// 因为obj有两种类型的情况,不能确保obj上存在name,所以使用in做守卫
if('name' in obj) {
obj.name.trim() // OK
}
}
// in操作符做类型守卫
function fn3(obj: { name: string } | { age: number }) {
// 因为obj有两种类型的情况,TS不能确保obj上存在的是name,所以使用in做守卫
if('name' in obj) {
obj.name.trim() // OK
}
}
// instanceof做类型守卫,原理同上
function fn4(date: Date | string) {
if(date instanceof Date) {
date.toDateString() // OK
}
}
自定义类型守卫
// 首先定义了一个isDate的工具函数
function isDate1(date: any) {
return typeof date === 'string'
}
function fn1(date: Date | string) {
if(isDate1(date)) {
// error,TS还没有智能到辨别isDate内做了类型守卫
return date.toDateString()
}
}
// 给isDate增加自定义类型守卫,返回类型为:date is Date
function isDate2(date: any): date is Date {
return typeof date === 'string'
}
function fn1(date: Date | string) {
if(isDate2(date)) {
return date.toDateString() // OK
}
}
泛型,重中之重!🙌
泛型相当于一个类型占位符,外部传入明确的类型将内部用泛型占位的地方替换掉
泛型基础用法
// 咱们来实现一个log函数,返回输入值本身,首先是没用泛型的实现
function log1(value: any) {
console.log(value)
return value
}
const returnValue1 = log1('str')
// 此处写slice时不会有类型提示,因为ts不知道返回值类型
returnValue1.slice(0, 1)
// 接下来是泛型的实现
// 这里的意思是,log函数声明了一个泛型叫T,value和返回值的类型都为T
function log2<T>(value: T): T {
console.log(value)
return value
}
// 这里指明了泛型T为string类型,就相当于把value参数和返回值指明为string类型
const returnValue2 = log2<string>('str')
// 此时访问slice时会有准确的补全提示,因为TS明确的知道返回值类型是string类型
returnValue2.slice(0, 1)
// 如果你指定的是string类型,当你传入其他类型的参数则会报错
const returnValue2 = log2<string>(123) // error
// 在以上这些简单情况下,这个log函数即使不指定泛型也是可以的,因为TS能推断出来类型
const returnValue3 = log2('str')
// 这里仍然会有补全提示
returnValue3.slice(0, 1)
泛型默认值
// 泛型默认值
function log<T = string>(value: T): T {
console.log(value)
return value
}
// 此处并没有指明泛型
log('12321321').slice(0, 1) // OK
泛型约束
泛型约束的目的是为了约束泛型的类型范围
// 通过extends关键字约束泛型的范围,以下约束泛型T为任意类型的数组
function shuffle<T extends any[]>(arr: T): T {
// 如果没有约束泛型未any[],则arr.sort会报错
return arr.sort(() => Math.random() - 0.5)
}
类泛型、接口泛型、自定义类型泛型
以下介绍以下其他泛型的用法,用法和以上函数泛型都差不太多
// 类泛型,定义
class Stack<T> {
private data: T[] = []
push(item: T) {
return this.data.push(item)
}
}
// 用法
const stack: Stack<string> = new Stack()
stack.push('hello') // OK
stack.push(123) // error
// 接口泛型,以下定义了一个后端接口数据基本格式,data的类型通过外部传入
interface Response1<T> {
code: 200 | 401 | 500
data: T
message: string
}
// 自定义类型泛型,和interface差不多
type Response2<T> = {
code: 200 | 401 | 500
data: T
message: string
}
// 还记的定义数组类型的第二种方式吗,这个就是用到了Array类的泛型
const arr: Array<string> = ['hello', 'world']
// 指定Promise泛型
const promise: Promise<string> = Promise.resolve('hello')
泛型实践
因为TS的泛型为重中之重,为了加深印象,再补充一个实践说明
以下代码为实现一个简易的ajax函数封装
interface RequestConfig {
url: string
method: 'get' | 'post'
params?: object
}
// 通常情况下,后端会定义一个基础的响应格式,具体的数据包含在data里面
interface BaseResponse<T> {
code: 200 | 401 | 500
data: T
message: string
}
// 给ajax函数定义了一个泛型,返回值为Promise类型,并将通过BaseResponse包裹后的泛型传递给了Promise
function ajax<T>({ url, method, params }: RequestConfig): Promise<BaseResponse<T>> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.send(JSON.stringify(params));
xhr.onreadystatechange = function () {
if(xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(JSON.parse(xhr.responseText))
}
}
}
})
}
const getUserList = () => {
return ajax<{
name: string
phone: string
}[]>({
url: '/users',
method: 'get',
})
}
getUserList().then(res => {
// 这里的res会有准确的类型提示,因为前面指定了泛型的类型
if(res.data.length > 0) {
console.log(res.data[0].name) // OK
}
})
函数重载
函数重载可应对当一个函数有多种入参类型的情况,这个特性在一些面向对象语言中常有提及
函数重载可以有多个定义签名,但只有一个实现签名
以下为了扩展上文ajax的用法,通过二次封装,让一些特殊情况使用更便捷
interface RequestConfig {
url: string
method: 'get' | 'post'
params?: object
}
// 上文封装的ajax
function ajax<T>({ url, method, params }: RequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.send(JSON.stringify(params));
xhr.onreadystatechange = function () {
if(xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(JSON.parse(xhr.responseText))
}
}
}
})
}
// 定义签名1
function request(url: string): void
// 定义签名2
function request(config: RequestConfig): void
// 实现签名
function request(urlOrConfig: string | RequestConfig) {
const requestConfig: RequestConfig = {
url: '',
method: 'get',
params: {}
}
if(typeof urlOrConfig === 'string') {
requestConfig.url = urlOrConfig
} else {
requestConfig.url = urlOrConfig.url
requestConfig.method = urlOrConfig.method
requestConfig.params = urlOrConfig.params
}
return ajax(requestConfig)
}
// 使用
request('/users') // OK
request({
url: '/users',
method: 'get'
}) // OK
// 当仅仅只是发起一个get请求,可以使用定义签名1的方式
TS内置类型
为了便捷性,TS内置了一些常用的工具类型
Extract\Exclude
- Extract,提取前面泛型中可以赋值给后面泛型的类型
// 源码
type Extract<T, U> = T extends U ? T : never;
// 使用
type Type = Extract<"a" | "b" | "c", "a" | "c" | "f"> // a" | "c"
- Exclude,从前面泛型中剔除可以赋值给后面泛型的类型
// 源码
type Exclude<T, U> = T extends U ? never : T;
// 使用
type Type = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f"> // b" | "d"
Record\Pick\Omit
- Record,生成给定字段的类型集合
// 源码
type Record<K extends string, T> = {
[P in K]: T;
}
// 使用
type User = {
name: string
age: number
}
type UserSet = Record<'ly' | 'yl', User>
// 得到以下结果
type UserSet = {
ly: User
yl: User
}
- Pick,选取某个类型中某些字段的类型,相当于生成一个子类型
// 源码
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
// 用法
type Phone = {
brand: string
price: number
camera: {
pixel: number
brand: string
}
}
type IPhone = Pick<Phone, 'price' | 'camera'>
// 得到以下结果
type IPhone = {
price: number
camera: {
pixel: number
brand: string
}
}
- Omit,和Pick相反,去除某个类型中某些字段的类型
// 源码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
// 用法
type Phone = {
brand: string
price: number
camera: {
pixel: number
brand: string
}
}
type IPhone = Pick<Phone, 'brand'>
// 得到以下结果
type IPhone = {
price: number
camera: {
pixel: number
brand: string
}
}
Partial\Required\Readonly
- Partial,将类型中所有属性变成可选类型
// 源码
type Partial<T> = {
[P in keyof T]?: T[P]
}
// 使用
type User = {
name: string
age: number
}
type UserPartial = Partial<User>
// 得到以下结果
type UserPartial = {
name?: string
age?: number
}
- Required,将类型中所有属性变成必选类型
// 源码
type Required<T> = {
[P in keyof T]-?: T[P]
}
// 使用
type User = {
name?: string
age?: number
}
type UserRequired = Required<User>
// 得到以下结果
type UserRequired = {
name: string
age: number
}
- Readonly,将类型中所有属性变成只读类型
// 源码
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// 使用
type User = {
name: string
age: number
}
type UserReadonly = Readonly<User>
// 得到以下结果
type UserReadonly = {
readonly name: string
readonly age: number
}
ReturnType
- ReturnType,提取函数返回值类型
// 源码
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
// 使用
type GetUserList = () => {
name: string
age: number
}[]
type UserList = ReturnType<GetUserList>
// 得到以下结果
type UserList = {
name: string
age: number
}[]
NonNullable
- NonNullable,排除null和undefined
// 源码
type NonNullable<T> = T extends null | undefined ? never : T
// 使用
type Type = 'hello' | null | 'world' | undefined
type Strs = NonNullable<Type>
// 得到以下结果
type Strs = 'hello' | 'world'
模块和命名空间
模块和命名空间的出现都是为了解决全局命名空间污染的问题,使得代码更具组织性。
// a.ts
const str = 'hello' // error,重复定义,因为在b.ts中也声明了
// b.ts
const str = 'hello' // error,重复定义,因为在a.ts中也声明了
模块
ts 和 es module 一致,带有顶级的import或者export声明的文件都被当作成一个模块,模块中定义的内容仅模块内可见,否则和最原始没有模块的js一样,任何文件中定义的变量都将成为全局变量。
以下为解决上述重复定义报错的问题
// a.ts
const str = 'hello' // OK
export {} // 增加一个export,让ts认为a.ts是一个模块
// b.ts
const str = 'hello' // OK
export {} // 增加一个export,让ts认为b.ts是一个模块
在ts中,import和export不仅可以导入导出变量、函数、类,还可以导入导出ts的interface、type等
// OK
// a.ts
export interface User1 {
name: string
age: number
}
// OK
export type User2 = {
name: string
age: number
}
// b.ts
import { User1, User2 } from './a' // OK
import type和export type
import type和export type用于导入导出ts的类型,不可用于导入导出值。
在现在的tsc中,已经可以根据定义自行识别导入的是值还是类型了;但对于其他如Babel编译ts,在一些情况下,是没法知道导入的是类型还是值。
// a.ts
export interface User {
name: string
age: number
}
export const message = 'hello'
// b.ts
import { User, message } from './a'
// 这种情况下,对于一些不那么智能的编辑器,是没法知道导入的是一个类型还是一个值
// 如果是使用Babel进行编译,这样使用可能会导致错误。
ts中的类型编译成js后都会被删除,所以在使用import type导入值的时候,在使用时会报错,如下:
- 使用import type导入枚举,在使用时会报错,因为枚举会生成一个js的值
// a.ts
export enum Direction {
Up,
Down,
Left,
Right,
}
// b.ts
import type { Direction } from './a'
// "Direction" 是使用 "import type" 导入的,因此不能用作值。
Direction.Down // error
- 导入的类只会当成一个类型
// a.ts
export class Person {
constructor(public name: string, readonly public age: number) {}
}
// b.ts
import type { Person } from './a'
const person1: Person = {
name: 'ly',
age: 18
} // OK
// "Person" 是使用 "import type" 导入的,因此不能用作值。
const person2 = new Person() // error
总结: 在现有的tsc编译器,单纯的使用import和export已经能满足需要; 如果为了看起来更直观或需要为一些编译器提供更好的基础,那么使用import type和export type也是可以的。
命名空间
-
命名空间和模块同样可以解决全局命名空间污染的问题,但它很难去识别模块之间的依赖关系,所以通常而言是使用模块,而不是命名空间。
-
命名空间的原理就是在全局声明了一个js对象,命名空间中的内容被包裹在这个对象中。
-
命名空间可以存在于多个文件,一个文件中也可以存在多个命名空间。
// a.ts
namespace A {
const str = 'hello' // OK
}
// b.ts
namespace A {
const str = 'hello' // OK
}
你可能会好奇,为什么两个命名空间都叫A,为什么可以重复声明str;
因为str是没有被导出的,所以str只作用于当前文件的A命名空间。
// a.ts
namespace A {
export const str = 'hello' // error,无法重新声明块范围变量“str”。
}
// b.ts
namespace A {
export const str = 'hello' // error,无法重新声明块范围变量“str”。
}
未导出的变量只在命名空间内部可访问:
// a.ts
namespace A {
const str = 'hello'
function log() {
console.log(str) // OK
}
}
A.str // error,因为str未导出
导出的变量可以在外部使用:
// a.ts
namespace A {
export const str1 = 'hello'
}
A.str1 // OK
A.str2 // OK,同样可以,尽管str2定义在别的文件,但是被导出了
// b.ts
namespace A {
export const str2 = 'world'
}
优先级:
// a.ts
namespace A {
const str = 'hello'
function test() {
console.log(str) // 输出:hello,因为当前文件中的str优先级高
}
}
// b.ts
namespace A {
export const str2 = 'world'
}
声明文件
声明文件是为 JS 代码提供类型声明,以.d.ts 结尾的文件就是声明文件。
基本用法
// xx.js
const env = 'development'
function log(value) {
console.log(value)
}
class Person {
constructor(name, age) {
this.name = name
this.name = age
}
}
const utils = {
deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
},
removeRepate(arr) {
return [...new Set(arr)]
}
}
// xx.d.ts
declare const env: 'development' | 'production'
declare function log(value: any): void
declare class Person {
constructor(public name: string, readonly public age: number)
}
declare const utils: {
deepClone: <T>(obj: T) => T,
removeRepate: <T extends any[]>(arr: T) => T,
}
// xx.ts
env // OK
log('hello') // OK
new Person('ly', 18).name // OK
utils.deepClone({ msg: 'hello' }).msg // OK
utils.removeRepate([1,2,3,1]) // OK
给模块提供声明
当我们在ts中使用第三方库时,可能会出现这样的提示:找不到模块“xxx”或其相应的类型声明,比如lodash、jquery、express等,因为他们都是用js写的,ts无法识别到这些库的类型;
所以需要单独为其提供声明文件,我们首先可以尝试安装ts官方提供的声明文件,如安装lodash的声明文件:npm i @types/lodash,
如果ts官方没有为某个库写声明文件的话,那么就需要我们自己来提供了。
// xx.d.ts
// 将lodash声明为any类型
declare module 'lodash'
// 详细点
declare module "lodash" {
function chunk<T>(array: List<T> | null | undefined, size?: number): T[][]
}
事实上,ts官方为js的所有变量、方法和类等都编写了声明文件,所以我们在ts中调用js方法能获得类型提示。
面向对象
ts为js扩展了很多面向对象的特性:
- public:修饰类属性为公共属性
- private:修饰类属性为私有属性,仅允许在类内部访问
- protected:修饰类属性为受保护属性,仅允许在类内部和子类中访问访问
- static:静态属性修饰符,和js的一致
- readonly:只读修饰符
- abstract:抽象类修饰符
属性修饰符
class Person {
public name: string
readonly age: number
protected job: string
private money: number
constructor(name: string, age: number, job: string, money: number) {
this.name = name
this.age = age
this.job = job
this.money = money
}
}
const person = new Person('ly', 18, 'coder', 1000000000)
person.name = 'yl' // OK
person.age = 19 // error
person.job // error
person.money // error
class Man extends Person {
static gender = 1
getJob() {
return this.job // OK
}
getMoney() {
// 属性“money”为私有属性,只能在类“Person”中访问。
return this.money // error
}
}
Man.gender // OK
构造函数初始属性简写
class Person {
constructor(public name: string, private age: number) {}
}
// 以上简写等于
class Person {
public name: string
private age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
抽象类
抽象类只能作为其他类的父类,不能被实例化,相当于定义派生类的框架
abstract class Person {
constructor(public name: string, private age: number) {}
}
new Person() // error,无法创建抽象类的实例。
class Man extends Person {}
new Man('ly', 18) // OK
抽象方法
抽象类中定义的抽象方法,在派生类中必须实现
abstract class Person {
constructor(public name: string, private age: number) {}
abstract speak(msg: string): void
}
class Man1 extends Person {} // error,因为派生类未实现抽象方法speak
class Man2 extends Person {
speak(msg: string): void {
console.log(msg)
}
} // OK
implements
implements用于约束class的实现
interface PersonInfo {
name: string
age: number
}
class Person implements PersonInfo {
constructor(public name: string) {} // error,因为Person类中缺少PersonInfo定义的age属性
}
三斜线指令
三斜线指令可以导入声明文件和其他模块
// 导入一个声明文件
/// <reference path="..." />
// 导入其他模块
/// <reference types="..." />
// 把文件标记成默认库,告诉编译器在编译过程中不要包含这个默认库
/// <reference no-default-lib="true"/>
// 用于给编译器传入一个可选的模块名
/// <amd-module name='NamedModule'/>
// 告诉编译器有一个非TypeScript模块依赖需要被注入,做为目标模块require调用的一部分。
/// <amd-dependency />
杂项
typeof
ts中的typeof和js的typeof概念有所不同,ts的typeof可用于获取变量的ts类型
const user = {
name: 'ly',
age: 18
}
type User = typeof user
// 得到以下结果
type User = {
name: string;
age: number;
}
keyof
keyof可以获取类型的属性名的联合类型
const user = {
name: 'ly',
age: 18
}
type User = typeof user
type UserKey = keyof User
// 得到以下结果
type UserKey = "name" | "age"
in
in就像是遍历一样
type Users = 'ly' | 'yl'
type UserSet = {
[P in Users]: string
}
// 得到以下结果
type UserSet = {
ly: string
yl: string
}
infer
infer可以做类型推断
// 如果泛型T继承数组类型,那么返回数组元素的类型,否则返回泛型T
type InferArr<T> = T extends Array<infer E> ? E : T
type UserArr = {
name: string
age: number
}[]
type User = InferArr<UserArr>
// 得到以下结果
type User = {
name: string
age: number
}