先去做几道题
首先,我写的这篇分享,是写给有一定ts基础,但关键时刻觉得对ts懵懵懂懂的朋友。其实我自己也是,共勉共勉,分享一些我的经验。先来几道题吧,感觉做题还是记东西最好的手段。点这里 -> typescript exercises
如何编写声明文件
第三方的模块,声明文件分为两种,一种是写在@types/
下的,一种是写在模块自身项目里的。我们在引用一个第三方包时,要查一下这个包是否有声明文件,没有的话,就要自己编写了。有如下几种情况:
- 全局类库编写
// globalLib.js
function globalLib(options) {
console.log(options)
}
globalLib.version = '1.2.1'
globalLib.doSomthing = function() {}
然后在globalLib.js的相同的目录下,新增一个globalLib.d.ts的声明文件
// globalLib.d.ts
// 全局声明
declare function globalLib(options: globalLib.Options): vold;
// 命名空间(函数与命名空间声明合并)
declare namespace globalLib {
const version: string;
function doSomthing(): void;
// 可索引类型的接口,可以接受任意的字符串属性,返回值是any
interface Options {
[key: string]: any
}
}
- 模块类库编写
// moduleLib.js
function moduleLib(options) {
console.log(options)
}
moduleLib.version = '1.2.1'
moduleLib.doSomthing = function() {}
module.exports = moduleLib
// moduleLib.d.ts
// 模块声明
declare function moduleLib(options: moduleLib.Options): vold;
// 声明文件本身是一个模块,所以该interface不会暴露
interface Options {
[key: string]: any
}
// 命名空间(函数与命名空间声明合并)
declare namespace moduleLib {
const version: string;
function doSomthing(): void;
}
export = moduleLib
- umd库编写
// umdLib.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory)
} else if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else {
root.umdLib = factory()
}
}(this, function() {
return {
version: '1.2.1',
doSomthing: function() {}
}
}))
// umdLib.d.ts
declare namespace umdLib {
const version: string;
function doSomthing(): void;
}
// 专门为umd库编写的语句,不可缺少!!!
export as namespace umdLib
export = umdLib
当以全局变量的方式引用umd库时,要把
tsconfig.json
中的allowUmdGlobalAccess
配置打开,不然会报错
- 给模块类库添加自定义方法
import moment from 'moment'
declare module 'moment' {
export function myFunction(name: string): string
}
moment.myFunction = (name: string) => name + 's'
- 给全局变量添加方法
declare global {
namespace globalLib {
function doAnything: void
}
}
globalLib.doAnything = () => {}
编译项详解
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
files | array | 编译器需要编译的单个文件的列表 | |
include | array | 编译器需要编译的文件或目录,支持通配符,比如:src/,表示只编译src目录下的一级目录,不包含二级目录。src//*表明只编译二级目录的ts文件。如果是src的话,就表示编译src目录下的所有ts文件 | |
exclude | array | node_modules下的文件,以及所有声明文件 | 编译器需要编译的单个文件的列表 |
extends | string | 配置可继承,如'./tsconfig.base.json' | |
incremental | boolean | 增量编译,可优化编译速度,第二次编译速度会有很大提升 | |
diagnostics | boolean | 打印编译的诊断信息 | |
tsBuildInfoFile | string | 增量编译文件的名称位置 | |
traget | string | ES3 | 指定ECMAScirpt目标版本:ES3,ES5,ES6(ES2015),ES2016,ES2017 或 ESNext |
module | string | traget === 'ES6' ? 'ES6' : 'commonjs' | 指定生成代码的模块标准: None,CommonJS,AMD,System,UMD,ES6,ESNext |
lib | string[] | 默认注入的库为:(1) 针对于--target ES5:DOM,ES5,ScriptHost (2) 针对于--target ES6:DOM,ES6,DOM.Iterable,ScriptHost | 编译过程中需要引入的库文件列表:ES5,ES6(ES2015),ES7(ES2106),ES2017, ES2018,ESNext,DOM,DOM.Iterable,WebWorker,ScriptHost,ES2015.Core,ES2015.Collection等等,可详见typescript源码。 |
allowJs | boolean | false | 允许编译js文件 |
jsx | string | preserve | 在 .tsx文件里支持JSX:'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. |
outDir | string | 重定向输出目录 | |
rootDir | string | 仅用来控制输出的目录结构 --outDir | |
moduleResolution | string | module === "AMD" or "System" or "ES6" ? "Classic" : "Node" | 决定如何处理模块解析 |
baseUrl | string | 解析非相对模块名的基准目录 | |
paths | Object | 模块名到基于 baseUrl 的路径映射的列表 | |
types | string[] | 要包含的类型声明文件名列表。如果指定了types,只有指定的内容回去@types/下查找,如果没指定types,则都会去@types下查找 | |
composite | boolean | 支持工程引用 |
- 下面来解析一下
noImplicitThis
这个配置,不允许this有隐式的any类型。
class A {
constructor() {
this.a = 'Lizzy'
}
getA() {
return function() {
console.log(this.a)
}
}
}
const foo = new A().getA()
foo()
此时,在控制台执行这段代码会报错,因为this的指向变了。此时需要改成箭头函数,就不会有问题。配置noImplicitThis
为true
,在代码编写阶段,编译器就会报错,可以避免这类的错误发生。
class A {
constructor() {
this.a = 'Lizzy'
}
getA() {
return () => {
console.log(this.a)
}
}
}
const foo = new A().getA()
foo()
- 再来看一个
moduleResolution
,typescript模块解析策略有classic(AMD、SystemJs,ES6)和node的解析策略,查找文件的查找路径会有所不同,如下图:
如下给出vue3中的ts配置,作为举例说明:
{
"compilerOptions": {
"baseUrl": ".", // 解析非相对模块的基地址,默认当前目录
"outDir": "dist", // 指定输出文件的目录
"sourceMap": false, // 生成目标文件的sourceMap
"target": "esnext",
"module": "esnext",
"moduleResolution": "node", // 模块的解析策略
"allowJs": false,
"strict": true, // 开启所有严格的类型检查 -> 在代码中注入use strict,不允许有隐式的any类型,不允许把null,undefined赋值给其他类型的变量,不允许this有隐式的any类型
"noUnusedLocals": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"removeComments": false, // 删除注释
"jsx": "preserve",
"lib": ["esnext", "dom"],
"types": ["jest", "puppeteer", "node"], // 控制声明文件目录,如果指定了,则只查找指定的声明文件
"rootDir": ".", // 指定输入文件目录
"paths": { // 路径映射,相对于baseUrl
"@vue/*": ["packages/*/src"],
"vue": ["packages/vue/src"]
}
},
"include": [
"packages/global.d.ts",
"packages/*/src",
"packages/runtime-dom/types/jsx.d.ts",
"packages/*/__tests__",
"test-dts"
]
}
Typescript核心库(lib)的类型详解
Typescript是把DOM、JavaScript内置对象等都定义好了类型,也就是上面配置项的--lib选项,可以配置编译过程中需要引入的库文件。
- DOM 例如,我们在用document.getElementById('app'),获取到app这个DOM元素时,它的类型在Typescript中就是:HTMLElement接口
如下,HTMLElement接口继承了Element, DocumentAndElementEventHandlers, ElementCSSInlineStyle, ElementCSSInlineStyle, ElementContentEditable, GlobalEventHandlers, HTMLOrSVGElement
这些接口,然后declare var HTMLElement: {}
声明了全局变量。我们可以通过查看Typescript里面的这些接口,快速了解DOM对象的结构,也不失为一种有趣的学习方式。
源码如下:
/** Any HTML element.
Some elements directly implement this interface,
while others implement it via an interface that inherits it. */
interface HTMLElement extends Element, DocumentAndElementEventHandlers, ElementCSSInlineStyle, ElementCSSInlineStyle, ElementContentEditable, GlobalEventHandlers, HTMLOrSVGElement {
accessKey: string;
readonly accessKeyLabel: string;
autocapitalize: string;
dir: string;
draggable: boolean;
hidden: boolean;
innerText: string;
lang: string;
readonly offsetHeight: number;
readonly offsetLeft: number;
readonly offsetParent: Element | null;
readonly offsetTop: number;
readonly offsetWidth: number;
spellcheck: boolean;
title: string;
translate: boolean;
click(): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
declare var HTMLElement: {
prototype: HTMLElement;
new(): HTMLElement;
};
针对以上源码,有几个知识点,我在此列出:
declare var
声明全局变量- 有一些elements直接实现了HTMLElement这个接口,然而还有一些是实现了继承自HTMLElement的接口:
再比方,我们用document.createElement('div')
生成的div,就是一个HTMLDivElement
接口。它的继承关系如下图:
Typescript中的lib.dom.d.ts
声明文件,已经定义好了这些关系的声明。
interface HTMLDivElement extends HTMLElement {
/**
* Sets or retrieves how the object is aligned with adjacent text.
*/
/** @deprecated */
align: string;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
declare var HTMLDivElement: {
prototype: HTMLDivElement;
new(): HTMLDivElement;
};
ESNext
Typescript 中的 ESNext 包含了es2020,esnext.intl等,如下三斜线指令可看出:
/// <reference lib="..." />
,该指令允许文件显式包含现有的内置 lib 文件。
/// <reference no-default-lib="true"/>
该指令将文件标记为默认库。您将在 lib.d.ts 及其不同变体的顶部看到此注释。
- 模块依赖:
/// <reference types="sizzle" />
,查找方式是在node_modules/@types/
目录下查找sizzle目录下的相应声明文件 - 路径依赖:
/// <reference path="legacy.d.ts" />
会在该文件同级的目录下找相应的声明文件legacy.d.ts
/// <reference no-default-lib="true"/>
/// <reference lib="es2020" />
/// <reference lib="esnext.intl" />
/// <reference lib="esnext.string" />
/// <reference lib="esnext.promise" />
/// <reference lib="esnext.weakref" />
Typescript内置工具类型
下面我来介绍一些工具泛型使用及其实现, 这些泛型接口定义大多数是语法糖(简写), 你可以在 typescript 包中的 lib.es5.d.ts
中找到它的定义。
根据使用范围,我们可以将工具类型划分为操作接口类型、联合类型、函数类型、字符串类型这四个方向。如下我只给出了实现该工具类型的源码,其实大家可以尝试着自己实现一次,对ts的提升会有很大的帮助。
操作接口类型
也就是说,这些工具类型泛型变量是接口类型
Partial
可以将一个类型的所有属性变为可选的
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
Required
将给定类型的所有属性变为必填,如下,映射类型在键值的后面使用了一个 - 符号,- 与 ? 结合起来表示去除类型的可选属性,因此给定类型的所有属性都变为必填了。
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
Readonly
将给定类型的所有属性设置为只读
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Pick
从给定类型中选取出指定的键值,然后组成一个新的类型。如下,Pick工具类型接收了两个泛型参数,第一个T为给定的参数类型,第二个参数为需要提取的键值key。
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Exclude<T, U> = T extends U ? never : T;
Omit
与Pick类型相反,Omit工具类型的功能是返回去除指定的键值之后返回的新类型。有趣的是,看如下Omit的实现,是借助Pick类型加Exclude类型组合而成的。
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
操作联合类型
Exclude
Exclude的作用就是从联合类型中去除指定的类型。Exclude的实现使用了条件类型。如果类型T可被分配给类型U,则不返回类型T,否则返回类型T。仔细体会它与Omit的区别,Omit操作的是接口类型,而Exclude操作的是联合类型。
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
// 示例:
type T = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type NewPerson = Omit<Person, 'weight'>
// 相当于
type NewPerson = Pick<Person, Exclude<keyof Person, 'weight'>>
Extract
Extract类型的作用与Exclude正好相反,Extract主要用来从联合类型中提取指定的类型,换句话说是取出两个联合类型的交集,类似于接口操作类型中的Pick类型。
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
// 示例
type T = Extract<'a' | 'b' | 'c', 'b' | 'c'> // 'b' | 'c'
发散思维:基础Extract实现一个获取接口类型交集的工具类型,你会怎么写?
type Intersect<T, U> = {
[K in Extract<keyof T, keyof U>]: T[K]
}
NonNullable
Nonnullable 的作用是从联合类型中去除 null 或者 undefined 的类型。如果你对条件类型已经很熟悉了,那么应该知道如何实现 NonNullable类型了。
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;
// 等同于使用 Exclude
type NonNullable<T> = Exclude<T, null | undefined>
Record
Record的作用是生成接口类型,然后我们使用传入的泛型参数分别作为接口类型的key和value。
划重点:这里的实现限定了第一个泛型参数继承自keyof any。在ts中,keyof any 指代可以作为对象键的属性,目前js只支持
string、number、symbol
作为对象的键值。
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
函数类型
ConstructorParameters
ConstructorParameters 可以用来获取构造函数的参数,它的实现需要用到infer
关键字推断构造参数的类型。infer大概实现是:如果真实的参数类型和infer
匹配的一致,那么就返回匹配到的这个类型。
如下类型实现,ConstructorParameters 泛型接收了一个参数,并且限制了这个参数需要实现构造函数。再通过infer字段匹配了构造函数内的参数,并返回了这些参数。
/**
* Obtain the parameters of a constructor function type in a tuple(元组类型)
*/
type ConstructorParameters<T extends new (...args: any) => any>
= T extends new (...args: infer P) => any ? P : never;
// 举例说明
class Person {
constructor(name: string, age?: number) {}
}
type T = ConstructorParameters<typeof Person> // [name: string, age?: number]
Parameters
用来获取函数的参数并返回
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
ReturnType
用来获取函数的返回类型,如下限制了入参类型需要满足函数类型
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
InstanceType
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
字符串类型
模版字符串
Ts自4.1版本开始支持模版字符串字面量类型。为此,Ts也提供了Uppercase、Lowercase、Capitalize、Uncapitalize这4种内置的操作字符串的类型,如下。这4种操作字符串字面量工具类型的实现都是使用JavaScript运行时的字符串操作函数计算出来的。
type T = Uppercase<'Hello'>; // 'HELLO'
type T1 = Lowercase<T>; // 'hello'
type T2 = Capitalize<T1>; // 'Hello'
type T3 = Uncapitalize<T2> // 'hello'
在Vue3中使用Typescript
初始化vue+ts项目
-
第一种情况是,直接用@vue/cli去新建一个含有ts的项目
-
第二种情况是在已有项目中添加ts 新建成功后,我们可以看到,
- vue的script是
lang="ts"
defineComponent
是vue的一个帮助函数,提供更好的ts支持
<template>
<div class="about">
<h1>This is an about page</h1>
<div @click="a++">{{a}}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
let a = ref(0)
return {
a
}
}
})
</script>
options API的这些属性如何使用ts
data
:需要用as
关键字表明Object的详细结构props
:需要引入PropType
声明范型computed
:需要给出返回的计算属性的类型methods
:需要给出参数和返回值的类型
import { defineComponent, PropType, h } from 'vue'
interface NameListItem {
name: string,
age: number,
}
export default defineComponent({
data () {
return {
list: [{
name: 'lizzy',
age: 28
}, {
name: 'rock',
age: 32
}] as NameListItem[]
}
},
props: {
defaultNameItem: {
type: Object as PropType<NameListItem>
},
size: {
type: Number,
default: 4
}
},
computed: {
lizzy (): NameListItem {
return this.list[0]
}
},
methods: {
addPerson (newPerson: NameListItem) {
this.list.push(newPerson)
}
},
render () {
return this.list.map((item: NameListItem) => {
const slot = this.$slots.default
? this.$slots.default()
: []
return h('ul', {
key: item.name
}, [
h('li', item.name),
h('li', item.age),
slot.map((child, index) => {
return h('div', { class: `mt-${this.$props.size} color-${index}` }, child)
})
])
})
}
})