typescript实践
typescript 中文文档 (opens new window)
# 变量声明
let isDone: boolean = false
let decimal: number = 6
let color: string = 'blue'
// 数组,有两种写法
let list: number[] = [1, 2, 3]
let list: Array<number> = [1, 2, 3]
// 元组(Tuple)
let x: [string, number] = ['hello', 10]
// 枚举
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
let c: Color = Color.Green
// 不确定的可以声明为any
let notSure: any = 4
// 声明没有返回值
function warnUser(): void {
alert('This is my warning message')
}
let u: undefined = undefined
let n: null = null
// 类型永远没返回
function error(message: string): never {
throw new Error(message)
}
// 类型主张,就是知道的比编译器多,主动告诉编译器更多信息,有两种写法
let someValue: any = 'this is a string'
let strLength: number = (<string>someValue).length
let strLength: number = (someValue as string).length
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
实际上,ts 会在初始化的时候根据初始值推断类型,不一定要定义。如
let count = 4
console.log(count.length) // Property 'length' does not exist on type 'number'.
2
# 接口
接口可以约束变量的结构。
interface SystemConfig {
attr1: number
attr2: string
func1(): string
}
const config: SystemConfig = {
attr1: 1,
attr2: 'str',
func1: () => '',
}
2
3
4
5
6
7
8
9
10
注意,接口中的函数有多种写法,且参数必须写全。
interface Fn3 {
(num1?: number, num2?: number): number
}
interface Item {
fn1: () => number
fn2(): string
fn3: Fn3
}
const item: Item = {
fn1() {
return 1
},
fn2() {
return '2'
},
// fn3(num1 = 0, num2 = 0) {
// return num1 + num2
// }
fn3(num1 = 0) {
return num1 + 1
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Robot 类继承了 Base 类,并实现了 Machine 和 Human 接口。
interface Machine {
move(): void
}
interface Human {
run(): void
}
class Base {}
class Robot extends Base implements Machine, Human {
run() {
console.log('run')
}
move() {
console.log('move')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# type
type 和 interface 都可以用来声明类型,但有区别。
- 声明类型范围
interface 主要用于类型检查,约束结构,可以声明对象,类,函数。
而 type 可以声明任何类型,比如基础类型,联合类型,元组等。
// 基本类型
type count = number
// 联合类型
interface Dog {
name: string
}
interface Cat {
age: number
}
type animal = Dog | Cat
// 元组
type pet = [Dog, Cat]
2
3
4
5
6
7
8
9
10
11
12
13
14
扩展性方面
接口可以 extends、implements,从而扩展多个接口或类。
- interface extends interface
interface Person { name: string } interface User extends Person { age: number }
1
2
3
4
5
6- interface extends type
type Person = { name: string } interface User extends Person { age: number }
1
2
3
4type 没有扩展功能,只能交叉合并。
- type & type
type Person = { name: string } type User = Person & { age: number }
1
2- type & interface
interface Person { name: string } type User = { age: number } & Person
1
2
3
4同名表现
定义两个同名的接口会合并声明。
interface Person { name: string } interface Person { age: number }
1
2
3
4
5
6定义两个同名的 type 会出现异常
type User = { name: string } type User = { age: number }
1
2
3
4
5
6type 可以获取 typeof 返回的值作为类型
let div = document.createElement('div') type B = typeof div // HTMLDivElement
1
2
# 联合类型与类型保护
常见联合类型用法如下
function formatCommandline(command: string[] | string) {
let line = ''
if (typeof command === 'string') {
line = command.trim()
} else {
line = command.join(' ').trim()
}
// Do stuff with line: string
}
2
3
4
5
6
7
8
9
10
面对联合类型,在数据处理过程中,就必须对数据类型进行判别,才能使用。这就用到类型保护。有下面几种方式:typeof
,instanceof
, in
, 使用字面量类型
,
自定义类型
。通俗地说,就是 ts 利用 js 的机制或 ts 的语法对类型进行判断,并确认是对应类型后,进行编译。
# typeof
function doSome(x: number | string) {
if (typeof x === 'string') {
// 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
console.log(x.subtr(1)) // Error: 'subtr' 方法并没有存在于 `string` 上
console.log(x.substr(1)) // ok
}
x.substr(1) // Error: 无法保证 `x` 是 `string` 类型
}
2
3
4
5
6
7
8
9
# instanceof
class Foo {
foo = 123
common = '123'
}
class Bar {
bar = 123
common = '123'
}
function doStuff(arg: Foo | Bar) {
if (arg instanceof Foo) {
console.log(arg.foo) // ok
console.log(arg.bar) // Error
}
if (arg instanceof Bar) {
console.log(arg.foo) // Error
console.log(arg.bar) // ok
}
console.log(arg.common) // ok
}
doStuff(new Foo())
doStuff(new Bar())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 自定义类型
如果是自定义的接口,需要用到is
。
interface Foo {
foo: number
common: string
}
interface Bar {
bar: number
common: string
}
// 用户自己定义的类型保护!
function isFoo(arg: Foo | Bar): arg is Foo {
return (arg as Foo).foo !== undefined
}
// 用户自己定义的类型保护使用用例:
function doStuff(arg: Foo | Bar) {
if (isFoo(arg)) {
console.log(arg.foo) // ok
console.log(arg.bar) // Error
} else {
console.log(arg.foo) // Error
console.log(arg.bar) // ok
}
}
doStuff({ foo: 123, common: '123' })
doStuff({ bar: 123, common: '123' })
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
基于此,写一个自己遇到的:
按钮组件对外提供一个配置,结构如下:
;[
{
text: '新增',
click: () => {
console.log('新增')
add()
},
attr: {
type: 'success',
},
},
[
{
text: '下拉',
click: () => {
console.log('下拉')
},
children: [
{
text: '下拉1',
click: (item, index) => {
console.log('下拉1', item, index)
},
},
{
text: '下拉2',
click: () => {
console.log('下拉2')
},
},
],
},
],
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
可以看到,数组中有可能是 obj,也可能是数组包裹的 obj,那么在渲染的时候,就要判断是否数组。
types 如下:
import { ElButton } from 'element-plus'
export interface ConfigObj {
text?: string
click?: (data1?, data2?) => void
attr?: typeof ElButton | any
children?: ConfigObj[]
}
export type ConfigItem = ConfigObj | ConfigObj[]
2
3
4
5
6
7
8
9
10
组件如下:
<template>
<template v-for="(item, index) of config" :key="index">
<template v-if="isArray(item)">
<template v-for="(btnItem, btnIndex) of item" :key="btnIndex">
<OButton v-bind="btnItem.attr || $attrs" :item="btnItem" @click="tapBtn(btnItem.click)">
{{ btnItem.text }}
</OButton>
</template>
</template>
<OButton v-else v-bind="item.attr || $attrs" :item="item" @click="tapBtn(item.click)">{{ item.text }}</OButton>
<el-divider v-if="index !== config.length - 1" direction="vertical" />
</template>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import OButton from './OButton.vue'
import { ConfigObj, ConfigItem } from './types'
const attrs = useAttrs()
const props = defineProps({
config: {
type: Array as PropType<ConfigItem[]>,
default: () => [],
},
})
// 类型保护,否则ts会报错
const isArray = (item: ConfigItem): item is ConfigObj[] => {
return Array.isArray(item)
}
const tapBtn = (click) => {
typeof click === 'function' && click()
}
</script>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 枚举
收集所有可能的值。
enum Color {
Red,
Green,
Blue,
}
let col = Color.Red
console.log(col) // 0
// 也可以是字符串
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue',
}
let col = Color.Red
console.log(col) // red
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 泛型
泛型用于给成员之间提供有意义的约束,它是一种任意类型,但一旦确定,就明确类型了。
泛型一般用于偏底层的类型约束,比较抽象。
举例说明。
改造前:
class QueueNumber {
private data = []
push = (item: number) => this.data.push(item)
pop = (): number => this.data.shift()
}
const queue = new QueueNumber()
queue.push(0)
queue.push('1') // Error: 不能推入一个 `string` 类型,只能是 `number` 类型
2
3
4
5
6
7
8
9
10
改造后:
class Queue<T> {
private data: T[] = []
push = (item: T) => this.data.push(item)
pop = (): T | undefined => this.data.shift()
}
const queue = new Queue<number>()
queue.push(0)
queue.push('1') // Error: 不能推入一个 `string` 类型,只能是 `number` 类型
2
3
4
5
6
7
8
9
10
这样一来,扩展性就很好。如果要限制 string 类型,只需要将number
改为string
即可。
再看一个例子。
function add(x: string, y: string): string
function add(x: number, y: number): number
function add(x: string | number, y: string | number): string | number {
if (typeof x === 'string' && typeof y === 'string') {
return x + y
} else if (typeof x === 'number' && typeof y === 'number') {
return x + y
}
}
console.log(add(1, 2)) // 3
console.log(add('hello', ' world')) // 'hello world'
2
3
4
5
6
7
8
9
10
11
12
13
上面的代码用到了函数重载,即函数名相同,参数个数或类型不同。这里可以使用泛型替代:
// function add(x: string, y: string): string
// function add(x: number, y: number): number
function add<T>(x: T, y: T): T
2
3
泛型不仅可以应用于类和函数,也可以用在接口。再看一个例子
// 使用场景:
// 有submit1和submit2 两个函数,他们的入参结构非常相似 都有value属性
// 区别在于:submit1的value属性是number submit1的value属性是string
interface SubmitData1 {
value: number
}
interface SubmitData2 {
value: string
}
function submit1(data: SubmitData1) {}
function submit2(data: SubmitData2) {}
2
3
4
5
6
7
8
9
10
11
这 2 个接口结构非常相似,于是可以优化成:
interface SubmitData<T> {
value: T
}
function submit1(data: SubmitData<number>) {}
function submit2(data: SubmitData<string>) {}
2
3
4
5
6
# 命名空间
模块复杂时,可以使用 namespace。
namespace N {
export namespace NN {
export function a() {
console.log('N.a')
}
export interface B {
name: string
}
}
}
N.NN.a()
const data = {
name: 'a',
}
function namespaceFn(data: N.NN.B) {}
namespaceFn(data)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 感叹号(!)和问号(?)
感叹号用于告诉 typescript 编译器,对象一定存在,不需要提示警告(慎用)。
全称是非空断言操作符 (opens new window),官方示例如下。
interface Entity {
name: string
}
// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
// Throw exception if e is null or invalid entity
}
function processEntity(e?: Entity) {
validateEntity(e)
let s = e!.name // Assert that e is non-null and access name
}
2
3
4
5
6
7
8
9
10
11
12
问号则用于对象属性的可选链式调用,它是 es6 的特性。
const obj = {
hh: 'xxx',
}
let res = obj?.data?.list
// 等价于
let res = obj && obj.data && obj.data.list
2
3
4
5
6
7
# 未知属性的对象
如果一个对象的属性和类型都不确定,可以使用下面的接口定义:
interface ObjectTy {
[x: string]: unknown
}
2
3
# tsconfig.json
tsconfig.json 存在于项目的根目录,用于配置 ts 编译选项,包括需要编译的文件和编译方式。
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
},
// 显示指定需要编译的文件
"files": ["./some/file.ts"],
// 指定需要包含的文件
"include": ["./folder"],
// 指定需要排除的文件
"exclude": ["./folder/**/*.spec.ts", "./folder/someSubFolder"]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
下面是基于 vite 模板的精简版本:
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"paths": {
"@/*": ["src/*"],
"~/*": ["typings/*"]
},
"types": ["element-plus/global"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# .d.ts
和declare
declare 表示声明,通常可以声明类型、变量和模块,需要在tsconfig.json
的 include 配置声明所在的文件,支持 glob 通配符。声明之后,其他地方就可以不用 import 了,直
接使用。同理,.d.ts
文件表示里面是声明的集合。
一句话解释 delcare:
declare 就是告诉 TS 编译器,你担保这些变量和模块存在,并声明了相应类型,编译的时候不需要提示错误。
注意:
1.使用时不需要引入,但是要确保该文件在src目录下。
2.不要在.d.ts文件中使用export导出,否则其他顶级声明会失效。
举例:
// tsconfig.json
"include": ["src/**/*.ts", "src/**/*.d.ts","typings"]
2
// typings/test.d.ts
declare interface Person1 {
name: string
}
type Person2 = {
name: string
}
declare const a_a = 'abc'
declare namespace MyName {
interface ABC {
name: string
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/index.ts
const test1 = (p: Person1) => {
console.log(p)
}
const test2 = (p: Person2) => {
console.log(p)
}
console.log(a_a)
const laugh = (person: MyName.ABC) => {
console.log(person)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 推荐插件:
# 根据接口数据快速生成接口
JSON to TS
与后端联调的时候,请求参数和返回字段很多的时候,一个个录入 ts 类型是非常痛苦的事。有了它,可以直接复制 json 数据,然后新建一个空文件,shift + ctrl + P
,找
到JSON to TS
点击即可生成 ts 类型。
# 建议:
- 类型断言需谨慎。因为使用断言(as)后,ts 就不会报错了,等于失去了这部分警告提示。
- 尽量不使用 any 类型,如果确实不知道类型,可以使用 unkown 代替。