TS | TypeScript 快速一览,可能有你不知道的哦👍

591 阅读17分钟

类型

基础类型

基础类型过一下

类型说明
string字符串
number数值
boolean布尔
null
undefined未定义
SymbolES6新增的基础类型,表示独一无二的值,可以用来定义私有变量
BigIntJS第七种基础数据类型,更大精度的整数
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
}