思考一个问题,JavaScript是一门非常优秀的编程语言,但是直到今天,JavaScript在类型检测上依然是毫无进展,所以我们需要学习TypeScript,这不仅仅可以为我们的代码增加类型约束,而且可以培养我们前端程序员具备类型思维

一、认识TypeScript
GitHub说法:TypeScript is a superset of JavaScript that compiles to clean
TypeScript官网:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
翻译一下:TypeScript是拥有类型的JavaScript超集,它可以编译成普通、干净、完整的JavaScript代码。
- JavaScript所拥有的特性,TypeScript全部都支持
- 在语言层面上,不仅仅增加了类型约束,而且包括一些语法的扩展
- TypeScript在实现新特性的同时,总是保持和ES标准的同步甚至是领先
- TypeScript最终会被编译成JavaScript代码,不需要担心兼容性问题
- TypeScript不仅让JavaScript更加安全,而且给它带来了诸多好用的好用特性
安装TypeScript
# 安装命令
npm install typescript -g
# 查看版本
tsc --version
二、配置TypeScript的环境
1. 使用ts-node
01 - 安装
// 安装ts-node及其依赖包
npm install ts-node tslib @types/node -g
02 - 创建 .ts 文件
// 代码栗子,创建 .ts 文件
// 规定message的类型为string
let message: string = 'hello'
// 报错!
// message = 123
// 规定参数类型为string
function abc(name: string){}
console.log(message);
// 因为ts默认作用域会在一个,这样设置导出,会让文件形成单独作用域
export {}
03 - 使用 ts-node
// ts-node可以直接运行TypeScript代码
ts-node 文件名

2. webpack进行配置
具体可看之前的webpack文章,webpack 之 零基础使用常用的Loader ,这里我快速过一遍哈
01 - 项目初始化
npm init -y
02 - 安装包
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin ts-loader typescript -D
03 - 新增index.html模版

04 - package.json配置

05 - webpack.config.js配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/main.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
devServer: {},
resolve: {
extensions: ['.ts', '.js', '.cjs', '.json']
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
};
06 - 生成tsconfig.json
// 先不用理这个文件,后续讲解
tsc --init
07 - 创建文件夹
创建src文件夹,里面再创建main.ts
const message: string = '123'
console.log(message);
08 - npm run serve运行即可
三、变量的声明
01. 声明变量的关键字
声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解
var/let/const 标识符: 数据类型 = 赋值;
数据类型的大小写是有区别的 ,用小写即可
- string ( 小写 ) 是TypeScript中定义的字符串类型
- String ( 大写 ) 是ECMAScript中定义的一个类
02. 类型推导/推断
类型推导 : 声明一个标识符时,如果有进行直接赋值,会根据赋值的类型推导出标识符的类型
ps :
let => 进行类型推导,推导出来通用类型
const => 进行类型推导,推导出来的是字面量类型

四、TypeScript的数据类型

1. number 类型
TypeScript和JavaScript一样,不区分整数类型(int)和浮点型(double),统一为
number类型
let demo: number = 22; // 十进制
let demo: number = 0b110; // 二进制
let demo: number = 0o555; // 八进制
let demo: number = 0xf23; // 十六进制
2. boolean 类型
boolean类型只有两个取值:true和false
let bool: boolean = true
let bool: boolean = 30 > 20
3. string 类型
string类型是字符串类型,可以使用单引号或者双引号表示,同时也支持ES6的模板字符串来拼接变量和字符串
const name: string = 'star'
const info: string = `my name is ${name}`
4. Array 类型
需要制定数组类型,同时制定数组内部值的类型
// 写法一 : 不推荐
const name: Array<string> = []
// 写法二 : 推荐
const name: string[] = []
let name1: string[] = ['张三', '李四', '王五'];
name1.push(1); // 报错, 只能添加字符串类型的数据
// 数组的类型最好是一致的,不一致的话,可以使用联合类型
let name2: (string | number)[] = ['张三', '李四', '王五'];
name2.push(1);
name2.push('赵六');
5. 对象 类型
// 1. 使用自动推导
const info = {
name: 'star',
age: 18
};
// 2. 手动写入
let info: {
name: string;
age: 20; // 这里的20是字面量类型,表示age只能是20
} = {
name: 'John',
age: 20 // 必须是20,否则报错
};
6. null 类型
const n: null = null
7. undefined 类型
const n: undefined = undefined
8. symbol 类型
// Symbol 生成独一无二的key
const info = {
[Symbol('name')]: 'star',
[Symbol('name')]: 'coder'
};
9. any 类型
无法确定一个变量的类型,并且可能它会发生一些变化,这个时候可以使用any类型
any类型有点像一种讨巧的TypeScript手段:
- 可以对any类型的变量进行任何的操作,包括获取不存在的属性、方法
- 给一个any类型的变量赋值任何的值,比如数字、字符串的值
什么时候使用any
- 如果对于某些情况的处理过于繁琐不希望添加规定的类型注解( 比如从服务器中获取到的数据 )
- 或者在引入一些第三方库时,缺失了类型注解
- 包括在Vue源码中,也会使用到any来进行某些类型的适配
// 任意类型都可赋值,也可赋值给任何类型
let message: any = 'star';
message = 123;
message = true;
// 任意类型的值,可以访问任意属性和方法
message.length;
message.toFixed();
10. unknown 类型
unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量
和any类型有点类似,但是unknown类型的值上做任何事情都是不合法的
ps : unknown进行任意操作前,必须进行类型校验(缩小),才能进行对应的操作
function foo() {
return 'string';
}
function bar() {
return 123;
}
let flag: boolean = true;
// result 不知是什么类型时可用
let result: unknown;
if (flag) {
result = foo();
} else {
result = bar();
}
/**
* unknown 类型的值不能直接使用
* result.length; // error
* 需要先判断类型,再使用 => 类型缩小
* if (typeof result === 'string') {
* result.length;
* }
*/
if (typeof result === 'string') {
result.length;
}
- unknown类型只能赋值给unknown类型和any类型
- any类型可以赋值给任何类型
- unknown类型是更加安全的any类型
let result: unknown;
// 报错
let num: number = result
// 可以赋值
let unres: unknown = result
let anyres: any = result
11. void 类型
当函数没有返回值的时候,该函数就是void类型,不写也阔以,会自动推导
// 如果返回值时void类型,也可以返回undefined
const sum = (num1: number, num2: number): void => {
console.log(num1, num2);
return undefined // 不会报错
};
12. never 类型
开发中很少实际去定义never类型 => 某些情况下会自动类型推导出never
永远没有返回值的时候,用never
// 其他时候在扩展工具的时候,对于一些没有处理的case,可以直接报错
function handleMessage(message: string | number | boolean) {
switch (typeof message) {
case 'string':
console.log('string', message.length);
break;
case 'number':
console.log('number', message.toFixed(2));
break;
default:
// 永远不会执行,因为上面的 case 已经覆盖了所有可能的值,赋值给never类型
const check: never = message; // error,Type 'boolean' is not assignable to type 'never'
}
}
handleMessage('hello world');
handleMessage(100.123);
/**
* 如果仅仅更改了参数类型,而没有更改 switch 的 case,TypeScript 会提示错误
*/
handleMessage(true);
13. tuple 元组类型
tuple和数组的区别
- 首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中
- 可以放在对象或者元组中
- 其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型
// tuple 多种元素的组合
// 1. 数组
const info1: any[] = ['star', 1, 2, 'coder'];
info1[0].slice(1); // 是any类型 => 虽然可以使用slice方法,但是不安全
// 2.1 元组
const info2: [string, number, number, string] = ['star', 1, 2, 'coder'];
info2[0].slice(1); // 可以直接知道是string类型 => 安全
// 2.2 元组类型的抽取
type infoType = [string, number, number, string];
const info3: infoType = ['star', 1, 2, 'coder'];
// 2.3 元组类型的使用,一般用于函数返回值
function useState(initState: number): [number, (newStateValue: number) => void] {
let stateValue: number = initState;
function setStateValue(newStateValue: number) {
stateValue = newStateValue;
}
return [stateValue, setStateValue];
}
// 可以明确知道返回值的类型,state是number类型,setState是一个函数
const [state, setState] = useState(0);
14. 函数的参数及返回值 类型
// 可指定函数的参数类型和个数和返回值的类型 name: string => 指参数类型为string string => 指函数的返回值类型为string,可不写,会推导
function getInfo(name: string): string {
return name;
}
// 匿名函数不需要写类型,TypeScript会根据forEach函数的类型以及数组的类型 推断出 item的类型
// 因为函数执行的上下文可以帮助确定参数和返回值的类型
const names = ['a', 'b', 'c'];
names.forEach((item) => {});
15. {} 对象类型
- 对象可以添加属性,并且告知TypeScript该属性需要是什么类型
- 属性之间可以使用 , 或者 ; 来分割,最后一个分隔符是可选的
- 每个属性的类型部分也是可选的,如果不指定,那么就是any类型
// info是一个对象类型,对象中有两个属性
function find(info: { name: string; age: number }) {}
16. ? 可选类型
对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?
// money是可选类型,可传可不传
function find(info: { name: string; age: number; money?: number }) {}
find({ name: 'coder', age: 123 });
find({ name: 'coder', age: 123, money: 1000 });
17. | 联合类型
TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型
联合类型(Union Type)
- 联合类型是由两个或者多个其他类型组成的类型
- 表示可以是这 类型中的任何一个值
- 联合类型中的每一个类型被称之为联合成员(union's members)
- 使用时,需使用 类型缩小判断类型后 再进行操作
// 联合类型 可以为其中的一种
// 1. 联合类型基本使用
let info: number | string = 1;
info = '1';
// 使用的时候需要判断类型
if (typeof info === 'string') {
console.log(info.length);
}
// 2. 联合类型的函数参数使用
function find(id: string | number) {
// 使用联合类型的值的时候,需要特别小心
switch (typeof id) {
case 'string':
break;
case 'number':
break;
}
}
find(123);
find('456');
可选类型和联合类型的关系
一个参数是一个可选类型的时候,它其实类似于这个参数是 类型|undefined的联合类型
function foo(message?: string) {}
// 这里可以不传
foo();
function foo(message: string | undefined) {}
// 但是这里还是要传一个值
foo(undefined);
18. 交叉类型 &
交叉类似表示需要满足多个类型的条件,交叉类型使用 & 符号
interface Ieat {
eating: () => void;
}
interface Idrink {
drink: () => void;
}
// 满足其中一个即可
type myType1 = Ieat | Idrink;
// 满足两个才可以 联合类型
type myType2 = Ieat & Idrink;
const obj1: myType1 = {
eating: () => {}
};
const obj2: myType2 = {
eating: () => {},
drink: () => {}
};
19. type 类型别名
// type 用于定义类型别名
type IdType = string | number | boolean;
// 太长了
function idSet(id: string | number | boolean) {}
// 取别名可以优化
function idSet(id: IdType) {}
20. as 类型断言
有时候TypeScript无法获取具体的类型信息,这个时候需要使用类型断言
// 默认是 HTMLElement 类型,范围太广了,有时会出错
// const oDom: HTMLElement = document.getElementById('star');
// <img id="star" />
// 用 as 指定是什么元素类型
const oDom: HTMLImageElement = document.getElementById('star') as HTMLImageElement;
oDom.src = 'url地址';
TypeScript只允许类型断言转换为 更具体 或者 不太具体( any/unknow ) 的类型版本,此规则可防止不可能的强制转换
/**
* 可以跳过类型检测,无敌,不推荐使用
*
* message先转成 any,再转成 number
* (message as any) as number
*
* 转换后无法正常使用
*/
const message = 'hello';
// 可以跳过类型检测,无敌,不推荐使用
const num: number = message as any as number;
console.log(num.toFixed(2)); // 运行时报错
21. ! 非空类型断言
如果确定传入的参数是有值的,这个时候可以使用非空类型断言
非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测
// 这样是没问题的
function printStrLength(message: string) {
console.log(message.length);
}
printStrLength('aaa');
printStrLength('11');
// 但是如果参数变成了可选类型呢,编译就会报错
function printStrLengthCan(message?: string) {
console.log(message.length);
}
// 解决方式一 : 类型缩小
function printStrLengthOne(message?: string) {
// 做个if判断即可
if (message) {
console.log(message.length);
}
}
// 解决方式二 : 非空断言(有点危险,确保有值时才使用)
function printStrLengthTwo(message?: string) {
// 加个非空断言 , 保证message一定有值
console.log(message!.length);
}
22. ?.可选链
可选链事实上并不是TypeScript独有的特性,它是ES11(ES2020)中增加的特性
- 可选链使用可选链操作符 ?.
- 它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行
// 定义一个类型
type Person = {
name: string;
// 可能有,可能没有
friend?: {
name: string;
// 可能有,可能没有
age?: number;
};
};
const info: Person = {
name: 'star'
};
const info2: Person = {
name: 'star',
friend: {
name: 'coder'
}
};
// 如果有friend,继续往后取,如果没有,返回undefined,相当于短路
console.log(info.friend);
console.log(info.friend?.name);
console.log(info.friend?.age);
// 类似于,可以节省很多代码
if (info.friend) {
if (info.friend.name) {
console.log(info.friend.name);
}
if (info.friend.age) {
console.log(info.friend.age);
}
}
export {};
23. !! 取boolean类型
const message: string = 'hello'
const flag: boolean = Boolean(message)
// !! 可以用作取反
const flag1: boolean = !!message
24. ??
// ?? 有值的时候取前面的值,没值的时候取后面的值
// 和三目运算符很像,不过更简洁一点
const message: string | null = null;
const res: string = message ?? 'hello';
console.log(res); // hello
const message1: string | null = 'coder';
const res1: string = message ?? 'hello';
console.log(res1); // coder
25. 字面量类型
字面量类型的类型和值要保持一致
const message = 'hello'; // 这个message的类型就是hello
let mess = 'hello' //这个mess的类型才是string
let me : 123 = 123
me = 456 // 报错
字面量类型的意义 : 必须结合联合类型
let align: 'left' | 'right' | 'center' = 'left'
// 再次修改值的时候,只能允许修改定义了类型的值
align = 'right'
26. 字面量推理
栗子
const info = {
url: 'www.baidu.com',
method: 'GET'
};
function request(url: string, method: 'GET' | 'POST') {
console.log(url, method);
}
// 这里info.method会报错,因为默认推导method是字符串类型,所以不能这么传
request(info.url,info.method)
解决方式一 : 推荐
// 定义一个类型
type requestType = {
url: string;
method: 'GET' | 'POST';
};
// 定义对象的时候就使用这个类型
const info: requestType = {
url: 'www.baidu.com',
method: 'GET'
};
function request(url: string, method: 'GET' | 'POST') {
console.log(url, method);
}
request(info.url, info.method);
解决方式二 :
const info = {
url: 'www.baidu.com',
method: 'GET'
};
function request(url: string, method: 'GET' | 'POST') {
console.log(url, method);
}
// 使用类型断言
request(info.url, info.method as 'GET');
解决方式三 :
function request(url: string, method: 'GET' | 'POST') {
console.log(url, method);
}
// 这里使用类型断言成const,其内部的属性值都变成了readeronly
const info = {
url: 'www.baidu.com',
method: 'GET'
} as const;
request(info.url, info.method);
export {};
27. 严格字面量赋值检测
对于对象的字面量赋值,在TypeScript中有一个非常有意思的现象
奇怪现象一
interface IPerson {
name: string;
age: number;
studying(this: IPerson): void;
}
// 1. 直接赋值
// const person1: IPerson = {
// name: 'coder',
// age: 18,
// studying() {
// console.log('studying');
// }
// // 新增属性会报错
// message:'hello'
// };
// 2. 间接赋值
const obj = {
name: 'star',
age: 18,
studying() {
console.log('studying');
},
message:'hello'
}
// 这样可以赋值成功
const person2: IPerson = obj;
console.log(person2); // { name: 'star', age: 18, studying: [Function: studying] }
奇怪现象二
interface IPerson {
name: string;
age: number;
studying(this: IPerson): void;
}
function foo(person: IPerson) {}
// 1. 直接赋值 => 新增属性会报错
// foo({ name: 'coder', age: 18, studying() {}, message: 'hello' });
// 2. 间接赋值 => 新增属性不会报错
const person = { name: 'coder', age: 18, studying() {}, message: 'hello' };
foo(person);
原因

interface IPerson {
name: string;
age: number;
studying(this: IPerson): void;
}
function foo(person: IPerson) {}
/**
* 解释现象:
* 第一次创建的对象字面量,称之为fresh object (新鲜对象)
* 对于新鲜的字面量,会进行严格的类型检测,必须完全满足接口的要求(不能多也不能少)
* 1. 所以直接赋值会报错,会被严格检测
* 2. 间接赋值,已经不是新鲜的字面量了,不会被严格检测
*/
// 1. 直接赋值 => 新增属性会报错
// foo({ name: 'coder', age: 18, studying() {}, message: 'hello' });
// 2. 间接赋值 => 新增属性不会报错
const person = { name: 'coder', age: 18, studying() {}, message: 'hello' };
foo(person);
28. 类型缩小
typeof 类型缩小
type idType = string | number | boolean;
function getId(id: idType): idType {
if (typeof id === 'string') {
return id;
}
if (typeof id === 'number') {
return id * 2;
}
if (typeof id === 'boolean') {
return id ? true : false;
}
}
平等 类型缩小
// 使用 === || == || !== || != || switch
type directionType = 'left' | 'right' | 'top' | 'bottom';
function getDirect(direct: directionType) {
if (direct === 'left') {
return 'x';
}
if (direct === 'right') {
return 'y';
}
if (direct === 'top') {
return 'z';
}
if (direct === 'bottom') {
return 'w';
}
// 或者这样亦可
switch (direct) {
case 'left':
return 'x';
case 'right':
return 'y';
case 'top':
return 'z';
case 'bottom':
return 'w';
}
}
instanceof 类型缩小
function foo(str: string | Date) {
if (str instanceof Date) {
str.getTime();
} else {
str.length;
}
}
class Student {
studying() {}
}
class Teacher {
teaching() {}
}
function boo(obj: Student | Teacher) {
if (obj instanceof Student) {
obj.studying();
} else {
obj.teaching();
}
}
in 类型缩小
type Fish = {
swimming: () => void;
};
type Bird = {
running: () => void;
};
function work(animal: Fish | Bird): void {
// 和对象中判断是否有某个属性一样
if ('swimming' in animal) {
animal.swimming();
}
if ('running' in animal) {
animal.running();
}
}
const fish: Fish = {
swimming: () => {
console.log('swimming');
}
};
const bird: Bird = {
running: () => {
console.log('running');
}
};
29. 枚举类型
枚举类型是为数不多的TypeScript特性有的特性之一:
- 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型
- 枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型
// 枚举这些数据
enum Direction {
Up, // 默认是0 可以更改默认值 up = 1,之后的值会自动加1 或者up = 'left'
Down, // 默认是1
Left, // 默认是2
Right // 默认是3
}
function trunDirection(dir: Direction): void {
switch (dir) {
case Direction.Up:
console.log('向上');
break;
case Direction.Right:
console.log('向右');
break;
case Direction.Down:
console.log('向下');
break;
case Direction.Left:
console.log('向左');
break;
default:
// 正常来说走不到这里,这样写就是为了防止枚举的数据越界,会报错
const never: never = dir;
}
}
trunDirection(Direction.Up);
trunDirection(Direction.Right);
trunDirection(Direction.Down);
trunDirection(Direction.Left);
五、TypeScript的函数详解
1. 函数的类型
函数类型表达式
/**
* 函数类型表达式
* 格式:(参数: 类型, 参数: 类型, ...) => 返回值类型
* 定义的时候
* 1. 形参的名字可以不和定义的参数名一致
* 2. 对于参数的个数不进行检测,可以少传,但是不能多传
* 3. 如果函数没有返回值,返回值类型可以是 void 或者省略
* 使用的时候
* 1. 参数的名字可以不和定义的参数名一致、
* 2. 参数个数一定要和定义的一致,不能少传,也不能多传
*/
type IncrementType = (x: number, y: number) => number;
// 1. 没有参数也可以
const increment: IncrementType = () => 0;
// 2. 参数的个数不进行检测,
const increment2: IncrementType = (x) => x;
const increment3: IncrementType = (x: number, y: number) => x + y;
// 3. 参数的个数可以少传,但是不能多传
// const increment4: IncrementType = (x: number, y: number, z: number) => x + y + z; // 报错,不能多传
// 使用的时候!必须传递两个参数,不能少传
console.log(increment(1, 2)); // 0
console.log(increment2(1, 2)); // 1
console.log(increment3(1, 2)); // 3
匿名函数
function foo() {
console.log('foo');
}
// 定义fn为函数类型 如果返回void,可以指不返回类型,可以指返回任意类型
type FnType = (num1: number, num2: number) => void;
function bar(fn: FnType) {
// 1. 调用fn,必须传入两个参数
fn(1, 2);
}
// 2. 调用bar,必须传入一个函数,且函数必须返回void,不一定要传入两个参数
bar(foo);
调用签名
从对象的角度看待函数,函数除了可以被调用,自己也是可以有属性值的
函数类型表达式并不能支持声明属性
想描述一个带有属性的函数,可以在一个对象类型中写一个调用签名(call signature)
注意这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是 : 而不是 =>
interface IBar {
age: number;
address: string;
// 告知这是一个函数,可以被调用 => 函数签名
// 格式 (参数列表): 返回值类型 ,这里是用冒号表示,而不是箭头
(num1: number, num2: number): void;
}
const bar: IBar = (num: number) => {
console.log(num);
};
bar.age = 18;
bar.address = '北京市';
// 使用的时候,必须传入两个参数
bar(1, 2); // 1
console.log(bar.age); // 18
console.log(bar.address); // 北京市
构造签名
JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为他们会产生一个新对象
可以写一个构造签名( Construct Signatures ),方法是在调用签名前面加一个 new 关键词
// 1. 定义一个类
class Person {}
// 2. 构造签名
interface IConstructor {
// 声明这个接口可以被new调用,返回值是Person类的实例
new (): Person;
}
/**
* 3. 定义一个函数,批量创建Person类的实例
* @param fn 接收一个类作为参数,该函数可以被new调用
* @returns 返回该类的实例
*/
function factory(fn: IConstructor) {
return new fn();
}
// 4. 调用函数
const p = factory(Person); // p是Person类的实例,可以调用Person类的方法
2. 参数的可选类型
以指定某个参数是可选的 : 该参数的类型为 指定的类型与undefined的联合类型
/**
* money是可选类型,可传可不传
* 可选的参数必须放在必传参数的后面
* 可选类型是 指定类型和undefined 的联合类型
*/
function find(info: { name: string; age: number; money?: number }) {
// 如果要使用可选类型的属性,需要先判断是否存在,因为可能不存在
if (info.money) {
console.log(info.money + 1000);
}
}
find({ name: 'coder', age: 123 });
find({ name: 'coder', age: 123, money: 1000 });
3. 参数的默认值
JavaScript是支持默认参数的,TypeScript也是支持默认参数的
默认参数的类型其实是 undefined 和 number 类型的联合
/**
* num2: 默认参数
* 有默认值的参数,可以不传,也可以传undefined,不会报错
*/
function foo(num1: number, num2: number = 20) {
console.log(num1 + num2);
}
foo(10);
foo(10, undefined);
foo(10, 30);
4. 剩余参数
// 第一个参数放到了initNumber,剩余的参数放到了nums数组中
function foo(initNumber: number, ...nums: number[]) {
return nums.reduce((a, b) => a + b, initNumber);
}
foo(10);
foo(10, 20);
foo(10, 20, 30);
foo(10, 20, 30, 40);
foo(10, 20, 30, 40, 50);
5. 函数的重载
编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用
一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现
// 函数的重载 : 函数名称相同,参数个数不同,参数类型不同
// 这里是函数的声明,没有函数体
function increment(num1: number, num2: number): number;
function increment(num1: string, num2: string): string;
// 这里是函数的实现,使用较为宽泛的类型
function increment(num1: any, num2: any): any {
// 如果是数字,则返回数字
if (typeof num1 === 'number' && typeof num2 === 'number') {
return num1 + num2;
} else if (typeof num1 === 'string' && typeof num2 === 'string') {
// 如果是字符串,则返回字符串长度
return num1.length + num2.length;
}
}
// 会自动匹配函数的参数类型
const result = increment(1, 2);
// 会自动匹配函数的参数类型
const result2 = increment('hello', 'world');
console.log(result, result2);
// 函数的重载中,实现函数是不能直接调用的
// 如果没有匹配的函数声明,则会报错
// increment({ name: 'name' }, { age: 18 });
6. 函数返回值类型和参数类型
可使用内置工具ReturnType获取函数的返回值类型
可使用内置工具Parameters获取函数的参数类型
/**
* ReturnType : 获取函数返回值的类型
* Parameters : 获取函数参数的类型
*/
type cals = (n1: number, n2: number) => number;
// 返回值类型
type calsReturnType = ReturnType<cals>; // number
// 参数类型
type calsParameters = Parameters<cals>; // [n1: number, n2: number]
function bar(str: string): string {
return 'foo';
}
// 返回值类型
type FooReturnType = ReturnType<typeof bar>; // string
// 参数类型
type FooParameters = Parameters<typeof bar>; // [str: string]
六、TypeScript的this类型
在没有对ts配置的情况下,this的使用可能存在隐患
可在tsconfig.json中进行配置
1. 函数中this默认类型
在没有指定this的情况,this默认情况下是any类型的
/**
* 在没有对ts配置的情况下,this的使用可能存在隐患
* this的类型是any
*/
const obj = {
name: 'demo',
studying() {
console.log(`${this.name} is studying`);
}
};
obj.studying(); // demo is studying
obj.studying.call({});
// TypeError: Cannot read property 'name' of undefined
function fn() {
console.log(this); // "this" 隐式具有类型 "any"
}
2. 进行tsconfig.json配置
在设置了noImplicitThis为true时, TypeScript会根据上下文推导this,但是在不能正确推导时,就会报错,需要明确的指定this
如果整个项目不想指定this类型,那么要设置为 noImplicitThis:false,注释可能没用

3. 指定函数中this的类型
函数的第一个参数类型
函数的第一个参数类型
- 函数的第一个参数可以根据该函数之后被调用的情况
- 用于声明this的类型(名词必须叫this)
- 在后续调用函数传入参数时,从第二个参数开始传递的,this参数会在编译后被抹除
// 第一个参数是this, 第二个参数开始才是函数的参数
function foo(this: { name: string }, info: {name: string}) {
console.log(this, info)
}
foo.call({ name: "why" }, { name: "kobe" })
4. this相关的内置工具
Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用
ThisParameterType
用于提取一个函数类型中的this 的类型
如果这个函数类型没有this参数返回unknown
function fn(this: { name: string }, age: number) {
console.log(this.name, age);
}
// 1. 拿到函数的类型
type fnType = typeof fn; // (this: { name: string }, age: number) => void 函数的类型
// 2. 通过 ThisParameterType 拿到 this 的类型
type thisType = ThisParameterType<fnType>; // { name: string } this的类型
OmitThisParameter
用于移除一个函数类型Type的this参数类型, 并且返回当前的函数类型
function fn(this: { name: string }, age: number) {
console.log(this.name, age);
}
// 1. 拿到函数的类型
type fnType = typeof fn; // (this: { name: string }, age: number) => void 函数的类型
// 2. 拿到删除this参数,剩余的函数类型
type pureFnType = OmitThisParameter<fnType>; // (age: number) => void
ThisType
这个类型不返回一个转换过的类型,它被用作标记一个上下文的this类型
用于绑定一个上下文的this
old
interface IState {
name: string;
age: number;
}
interface IStore {
state: IState;
eating: () => void;
}
const store: IStore = {
state: {
name: '张三',
age: 18
},
// 需要指定 this 的类型,比较麻烦
eating(this: IState) {
console.log(this.name); // 希望这里的 this 指向 state
}
};
store.eating.call(store.state); // 张三
new
interface IState {
name: string;
age: number;
}
interface IStore {
state: IState;
eating: () => void;
}
/**
* 通过 ThisType<T> 指定 this 的类型为 IState
* 使用交叉类型将 this 的类型和 store 的类型合并
* 该对象中的所有方法中的 this 都会被推断为 IState
*
* 相当于不用在每个方法中都写 this: IState!
*/
const store: IStore & ThisType<IState> = {
state: {
name: '张三',
age: 18
},
eating() {
console.log(this, this.name); // this 指向 state
}
};
// store.eating.call(store.state); // { name: '李四', age: 18 } 李四
store.eating.call({ name: '李四', age: 18 }); // { name: '李四', age: 18 } 李四
七、TypeScript的类
1. 类的封装 ( 定义 )
class Person {
/**
* ts中的类成员属性必须要声明后使用,普通的js中可以直接使用
* 可以给默认值,也可以在构造函数中初始化
*/
name: string;
age: number = 66; // 默认值,但是不知道有什么用,构造函数中初始化的值会覆盖默认值
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const p = new Person('Jack', 32);
console.log(p.name); // Jack
console.log(p.age); // 32
const p2 = new Person('Tom', 19);
2. 类的继承
// 父类
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eating() {
console.log('person eating~');
}
}
// 子类
class Student extends Person {
sno: string;
constructor(name: string, age: number, sno: string) {
// 调用父类的构造方法,必须写在第一行
super(name, age);
this.sno = sno;
}
// 子类重写父类的eating
eating() {
// 调用父类的方法
super.eating();
console.log('student eating~');
}
}
const s = new Student('star', 16, 't100010');
console.log(s.name); // star
console.log(s.age); // 16
console.log(s.sno); // t100010
s.eating(); // person eating~ student eating~
3. 类的多态
// 父类
class Person {
eating() {
console.log('person eating~');
}
}
// 子类
class Student extends Person {
eating() {
console.log('Student eating~');
}
}
// 子类
class Teacher extends Person {
eating() {
console.log('Teacher eating~');
}
}
// 多态的目的是为了写出更加具备通用型的代码
// 父类引用指向子类对象
function eatFunction(persons: Person[]) {
persons.forEach((person) => {
person.eating();
});
}
// 传入子类对象
eatFunction([new Student(), new Teacher()]);
4. 类的成员修饰符

public
class Person {
// 默认就是public
name: string;
public name2: string;
}
private
class Person {
private _name: string;
// 通过这样暴露属性出去
getName() {
return this._name;
}
// 通过这样更改属性
setName(name: string) {
this._name = name;
}
}
const p = new Person();
// 外面直接访问不了
// p._name
p.getName();
p.setName('star');
protected
class Person {
protected name: string;
}
class Student extends Person {
getName() {
// 可以直接访问父类中的name
return this.name;
}
}
const s = new Student();
s.getName();
// 外面直接访问不了
// s.name
5. 只读属性 readonly
class Person {
// 1.只读属性可以在构造函数中赋值,赋值之后就不能再更改了
// 2.属性本身不能进行修改,但如果是对象类型,那么对象中的属性是可以修改的
readonly name: string;
constructor(name: string) {
this.name = name;
}
}
const p = new Person('star');
// 不能修改
// p.name = '132';
6. getter - setter
私有属性是不能直接访问的,或者某些属性想要监听它的获取(getter)和设置(setter)的过程,这个时候可以使用存取器
可以对属性的访问和读取进行拦截操作
class Person {
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
}
set name(name: string) {
this._name = name;
}
get name() {
return this._name;
}
set age(age: number) {
// 限制年龄范围,不合法抛出异常
if (age < 0 || age > 200) {
throw new Error('年龄不合法');
}
this._age = age;
}
get age() {
// 可以拼接一些字符串,或者做一些其他操作
return this._age;
}
}
const p = new Person('star', 20);
console.log(p.name); // star
// 赋值
p.name = 'coder';
// 获值
console.log(p.name); // coder
// 限制年龄范围
p.age = 300; // Error: 年龄不合法
7. 参数属性
TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性
在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符
/**
* 参数属性 : 语法糖
* 1. 修饰符 + 参数名
* 2. 修饰符 + 参数名 + 类型
* 3. 修饰符 + 参数名 + 类型 + 可选标识符
*
* 修饰符: public, private, protected, readonly
*
* 相当于默认做了以下操作:
* 1. 在类中定义了一个同名的成员属性 => public name: string
* 2. 在构造函数中给成员属性赋值 => this.name = name
*/
class Person {
// 1. 定义了一个同名的成员属性
// public name: string;
constructor(public name: string) {
// 2. 在构造函数中给成员属性赋值
// this.name = name;
}
sayHello() {
return 'Hello, ' + this.name;
}
}
const person = new Person('World');
console.log(person.sayHello()); // Hello, World
8. 类的静态成员
class Person {
// 静态属性
static age: number = 18;
// 静态方法
static eating() {
console.log('eating');
}
}
// 可直接通过类名访问
console.log(Person.name);
Person.eating();
9. abstract 抽象类
abstract用来定于抽象类和抽象方法
继承是多态使用的前提
- 所以在定义很多通用的调用接口时, 通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,可以定义为抽象方法
抽象方法
- 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法
- 抽象方法,必须存在于抽象类中
- 抽象类是使用abstract声明的类
抽象类有如下的特点:
- 抽象类是不能被实例的话(也就是不能通过new创建)
- 抽象方法必须被子类实现,否则该类必须是一个抽象类
- 有抽象方法的类,必须是一个抽象类
- 抽象类可以包含抽象方法,也可以包含有实现体的方法
/**
* 抽象类中的抽象方法,必须在子类中实现,否则会报错
* 抽象类不能直接被实例化,只能被继承,抽象类中的抽象方法不能包含具体实现,只能用于接口
* 不能用于实例,抽象类中的抽象方法不能被static修饰,只能用于抽象类,不能用于实例
*/
// 抽象类,不能直接被实例化,只能被继承
abstract class Person {
// 抽象方法,具体实现由子类实现
abstract sayHello(): void;
// 抽象类中也可以有具体实现的方法,子类可以不实现,也可以实现,也可以重写,也可以调用
sayHi() {
console.log('Hi');
}
}
class Student extends Person {
constructor(private _studentName: string) {
super();
}
// 子类必须实现抽象类中的抽象方法,否则会报错
sayHello() {
console.log('Hello,Student', this._studentName);
}
}
class Teacher extends Person {
constructor(private _teacherName: string) {
super();
}
// 子类必须实现抽象类中的抽象方法,否则会报错
sayHello() {
console.log('Hello,Teacher', this._teacherName);
}
}
// 1. 普通的方式
const student = new Student('小明哥');
const teacher = new Teacher('decade');
student.sayHello(); // Hello,Student 小明哥
teacher.sayHello(); // Hello,Teacher decade
// 2. 多态的方式,父类的引用指向子类的对象
function saySomething(person: Person) {
person.sayHello();
person.sayHi();
}
saySomething(new Student('空我')); // Hello,Student 空我
saySomething(new Teacher('螺旋踢')); // Hello,Teacher 螺旋踢
10. 鸭子🦆类型
TypeScript对于类型检测的时候使用的鸭子类型
鸭子类型
- 走起来像鸭子,叫起来像鸭子,游起来像鸭子,那么就可以认为它就是一只鸭子
- 只要两个对象的结构相同,那么它们就可以相互赋值
- 只关心属性和行为,不关心具体是不是对应的类型
class Student {
constructor(public name: string) {}
sayHello() {
console.log('Hello,Student');
}
}
class Teacher {
constructor(public name: string) {}
sayHello() {
console.log('Hello,Teacher');
}
}
function printTeacher(people: Teacher) {
people.sayHello();
}
// 正常使用
printTeacher(new Teacher('老师')); // Hello,Teacher
/**
* 不会报错,
* 1. Student没有继承Teacher,传错了类型但是没有问题,因为使用了鸭子类型
* 只要满足有name属性和sayHello方法就可以传入
* 2. 甚至可以传入一个对象,只要满足有name属性和sayHello方法就可以传入
*/
printTeacher(new Student('小明')); // Hello,Student
printTeacher({
name: '小红',
sayHello() {
console.log('Hello,小红'); // Hello,小红
}
});
11. 类的类型
类本身也是可以作为一种数据类型的
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
eating() {
console.log(this.name + ' is eating');
}
}
const p1: Person = new Person('aaa');
p1.eating(); // aaa is eating
/**
* 因为是鸭子类型, 所以只要有name属性和eating方法就可以
* 定义p2为Person的类型,要求p2中必须有name属性和eating方法
*/
const p2: Person = {
name: 'bbb',
eating() {
console.log(this.name + ' is eating');
}
};
p2.eating(); // bbb is eating
八、TypeScript的接口
1. 声明对象类型
type
// 通过类型 (type) 来声明对象类型
type InfoType = {
name: string;
age: 18;
};
const info: InfoType = {
name: 'John',
age: 18
};
interface
/**
* 另外一种方式声明对象类型,使用 interface
* interface 用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法
* 同时 interface 也可以当成类型声明去使用,可以定义可选属性,也可以定义只读属性
*/
interface IInfoType2 {
readonly name: string;
age?: number;
}
const info2: IInfoType2 = {
name: 'John'
};
interface和type区别
区别一 : type类型使用范围更广,接口类型只能用来声明对象
// type 可以声明基础类型、联合类型、元组、枚举、类、接口、函数、对象等
type infoType = number | string;
type objType = {
name: string;
age: number;
};
// interface 可以声明接口、函数、对象等
interface infoInterface {}
区别二 : 在声明对象时,interface可以多次声明
/**
* type 只能声明一次, 不能重复声明,会报错
* 不允许两个相同名称的别名同时存在
*/
type objType = {
name: string;
age: number;
};
// err, 重复声明
// type objType = {
// address: string;
// }
const obj: objType = {
name: 'lison',
age: 18
};
/**
* interface 可以声明多次, 会进行合并
* 允许两个相同名称的接口同时存在
* 使用时,合并的属性会叠加,方法会进行合并,同名方法会被覆盖 => 必须都使用上
*/
interface infoInterface {
name: string;
}
interface infoInterface {
age: number;
address?: string;
}
const info: infoInterface = {
// name和age都必须使用,否则报错, address可选
name: 'lison',
age: 18
};
区别三 : interface可以实现继承
// interface可以实现继承
interface IPerson {
name: string;
age: number;
}
interface Ikun extends IPerson {
secret: string;
}
const smallBlack: Ikun = {
// name 和 age 和 secret 都是必须的
name: '小黑子',
age: 18,
secret: '鸡你太美!!!!!!'
};
区别四 : interface可以被类实现
// interface可以被类实现
interface IPerson {
name: string;
age: number;
}
class Person implements IPerson {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
总结
如果是非对象类型的定义 => 使用type
如果是对象类型的声明 => 使用interface,会更加灵活
type 和 interface根本区别 :
- interface 接口名可以重复,会合并。
- type 定义的类型名,不可重复,会报错
2. 对象类型的属性修饰符

3. 索引签名
不能提前知道一个类型里的所有属性的名字,但是知道这些值的特征
这种情况,可以用一个索引签名 (index signature) 来描述可能的值的类型
// 定义类型
interface IndexLanguage {
// 索引为number,值为string
[index: number]: string;
}
const language: IndexLanguage = {
1: 'typescript',
2: 'javascript',
3: 'python',
4: 'ruby',
5: 'c++'
// 报错,索引只能是number
// '7':1
};
console.log(language[1]); // typescript
console.log(language[2]); // javascript
4. 函数类型
// type calcFn = (n1: number, n2: number) => number;
interface ICalcFn {
// 定义一个函数类型
(n1: number, n2: number): number;
}
function calc(n1: number, n2: number, calcFn: ICalcFn): number {
return calcFn(n1, n2);
}
const add = (n1: number, n2: number): number => n1 + n2;
console.log(calc(20, 30, add));
5. 接口的继承
接口是支持多继承的(类不支持多继承)
interface Ieat {
eating: () => void;
}
interface Idrink {
drink: () => void;
}
// 可以多继承
interface action extends Ieat, Idrink {
sleep: () => void;
}
const person: action = {
// 都要实现
eating: () => {},
drink: () => {},
sleep: () => {}
};
6. 接口的实现
类可以实现多个接口
interface IPerson {
name: string;
age: number;
studying(this: IPerson): void;
}
/**
* implements 实现接口
* 属性和方法必须实现
*/
class Person implements IPerson {
age: number;
constructor(public name: string, age: number) {
this.age = age;
}
studying() {
console.log(this.name, 'studying');
}
}
const p = new Person('why', 18);
p.studying();
7. 接口的合并
interface IPerson {
eating: () => void;
}
interface IPerson {
drink: () => void;
}
// 相同的接口名会合并成一个接口
const p: IPerson = {
// 都要实现
eating: () => {},
drink: () => {}
};
8. 抽象类和接口的区别

九、TypeScript的泛型

1. 泛型的基本使用
- 在定义函数时,不决定参数的类型
- 是让程序员决定参数的类型,通过传入不同的参数来决定
- 格式: function 函数名<T>(参数:T):T{}
- <T> :传递的类型参数
- T相当于一个变量,用于记录传入的参数的类型,在整个函数执行过程中,T的类型是不变的
// 封装一个函数,传入一个参数,并且返回这个参数
function sum<T>(n: T): T {
return n;
}
/**
* 完整的写法
* 调用方式一 : 明确指定参数类型
* 在调用函数时,用明确的类型来代替T
*/
const n1 = sum<number>(1); // n1的类型是number
const n2 = sum<string>('str'); // n2的类型是string
const n3 = sum<boolean[]>([true, false]);
const n4 = sum<number[][]>([
[1, 2, 3],
[4, 5, 6]
]);
/**
* 简化的写法
* 调用方式二 : 不指定参数类型,类型推断,推出来的是字面量类型
* 在调用函数时,不用明确的类型来代替T,而是通过传入的参数来推断出T的类型
* const => 返回的是字面量类型
* let => 返回的是具体的类型
*/
const n5 = sum(1); // n5的类型是字面量类型1
let n6 = sum('str'); // n6的类型是string
2. 泛型的练习
/**
* 通过泛型来约束,传入的参数和返回值的类型
* @param initState state的初始值
* @returns 返回一个数组, 第一个元素是state, 第二个元素是setState
*/
function useState<T>(initState: T): [T, (newState: T) => void] {
let state = initState;
function setState(newState: T) {
state = newState;
}
return [state, setState];
}
const [count, setCount] = useState(0); // count: number | setCount: (newState: number) => void
const [age, setAge] = useState('jack'); // age: string | setAge: (newState: string) => void
3. 泛型接收多个参数

function foo<T, E, O>(arg1: T, arg2: E, arg3: O): [T, E, O] {
return [arg1, arg2, arg3];
}
// 1. 指定类型
const [a, b, c] = foo<number[], { name: string }, boolean>([1, 2], { name: 'why' }, true);
// 2. 类型推断
const res = foo(1, 'str', true); // [1, 'str', true] => [number, string, boolean]
4. 泛型接口
/**
* 泛型
* T: 类型变量
* O:类型变量,默认值为string
*/
interface Iperson<T, O = number> {
name: T;
age: O;
address: string;
}
// 传递类型变量,T为string,O为number,使用默认值
const p: Iperson<string> = {
name: '张三',
age: 18,
address: '北京'
};
// 传递类型变量,T为number,O为string
const p1: Iperson<number, string> = {
name: 11,
age: '22',
address: '北京'
};
5. 泛型类
class point<T> {
x: T;
y: T;
constructor(x: T, y: T) {
this.x = x;
this.y = y;
}
}
// 1. 指定泛型类型
const p1 = new point<number>(1, 2);
const p3: point<string> = new point<string>('1.11', '1.22');
// 2. 类型推断
const p2 = new point('1.11', '1.22');
// 3. 指定数组有两种,一种是元素类型后面加[], 一种是Array<元素类型>
const names1: string[] = ['Max', 'Manu'];
const names2: Array<string> = ['Max', 'Manu'];
6. 泛型的类型约束
例子一 : 简单约束
interface Ilength {
length: number;
}
// 规定传入的参数必须包含length属性
function getLength<T extends Ilength>(target: T): T {
return target;
}
// 都拥有length属性,但是不一定是字符串,也可以是数组,对象,其他类型,所以不能用类型断言,只能用接口
const r1 = getLength('123');
const r2 = getLength([1]);
const r3 = getLength({ length: 10, name: '123' });
例子二 : 复杂约束
/**
* keyof => 获取对象的所有key, 返回一个联合类型
*/
interface IKun {
name: string;
age: number;
}
type kunkun = keyof IKun; // "name" | "age"
/**
* K extends keyof O => K必须是O的key, 且K的类型必须是O的key的类型
* 把传入的k进行类型约束
* 确保不会传入一个不存在的key
*/
function getObjectProperty<O, K extends keyof O>(obj: O, key: K) {
return obj[key];
}
const kun: IKun = {
name: 'kun',
age: 18
};
const name = getObjectProperty(kun, 'name');
// const address = getObjectProperty(kun, 'address'); // 报错, 因为address不是kun的key
export {};
7. 映射类型
一个类型需要基于另外一个类型,但是你又不想拷贝一份,这个时候可以考虑使用映射类型
大部分内置的工具都是通过映射类型来实现的
大多数类型体操的题目也是通过映射类型完成的
映射类型建立在索引签名的语法上
- 映射类型,就是使用了 PropertyKeys 联合类型的泛型
- 其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型
基本使用
// 1. 定义一个接口
interface IPerson {
name: string;
age: number;
}
/**
* 2. 映射类型函数
* 2.1. 映射类型不能用于interface, 只能用于type
* 2.2 <Type> => 表示要对Type进行映射
* 2.3 keyof Type => 表示Type中的所有key组成的联合类型 => "name" | "age"
* 2.4 [K in keyof Type] => 表示对Type中的每一个key进行映射
*/
type MapToPerson<Type> = {
[K in keyof Type]: Type[K];
};
// 3. 使用映射类型,将IPerson中的所有属性都映射到新的类型中
type newPerson = MapToPerson<IPerson>; // {name: string, age: number}
//4. 定义一个对象,必须包含IPerson中的所有属性
const person: newPerson = {
name: 'why',
age: 18
};
映射修饰符
修饰符 :
- 一个是 readonly
- 用于设置属性只读
- 一个是 ?
- 用于设置属性可选
interface IPerson {
name: string;
age: number;
address: string;
}
/**
* readonly : 将所有的属性变成只读的
* ? : 将所有的属性变成可选的
*/
type MapToPerson<T> = {
readonly [k in keyof T]?: T[k];
};
/**
* 更改后的结果: 都变成了只读的可选属性
* readonly name?: string | undefined;
* readonly age?: number | undefined;
* readonly address?: string | undefined;
*/
type newPerson = MapToPerson<IPerson>;
还可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀
interface IPerson {
name: string;
age?: number;
readonly height: number;
address?: string;
}
/**
* -? : 删除属性的可选性
* -readonly : 删除属性的只读性
*/
type MapToPerson<T> = {
-readonly [k in keyof T]-?: T[k];
};
/**
* 删除后的结果: 所有属性都是必选的
* name: string;
* age: number;
* height: number;
* address: string;
*/
type PersonRequire = MapToPerson<IPerson>;
8. 条件类型

基本使用
/**
* 条件类型例子一
*/
// 1. 定一个类型
type IDType = string | number;
// 2. 条件类型 => T extends IDType ? true : false; => 三元表达式
type ResType = number extends IDType ? true : false;
console.log('-------------------------------------------------------------');
/**
* 条件类型例子二
* 函数重载
*/
// 1. 写法一 => 默认写法
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
return a + b;
}
const one1 = add(1, 2); // one1 => number类型 => 3
const onw2 = add('1', '2'); // onw2 => string类型 => '12'
// 2. 写法二 => 调用签名,函数实现签名
interface IAdd {
(a: number, b: number): number;
(a: string, b: string): string;
}
const increment: IAdd = (a: any, b: any): any => {
return a + b;
};
const two1 = increment(1, 2); // two1 => number类型 => 3
const two2 = increment('1', '2'); // two2 => string类型 => '12'
// 3. 写法三 => 使用条件类型
function sum<T extends number | string>(a: T, b: T): T extends number ? number : string;
function sum(a: any, b: any) {
return a + b;
}
const three1 = sum(1, 2); // three1 => number类型 => 3
const three2 = sum('1', '2'); // three2 => string类型 => '12'
在条件类型中推断
条件类型提供了 infer 关键词
可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果
ps : 在返回值处重新写一遍<T extends new (...args: any[]) => any>

/**
* 使用infer关键字
* 自定义封装一个ReturnType,获取函数的返回值类型
* 自定义封装一个Parameters,获取函数的参数类型
*/
// 使用TypeScript内置的ReturnType和Parameters
function bar(n: number): string {
return `${n}hello`;
}
// 函数的返回值类型
type FooReturnType = ReturnType<typeof bar>; // string
// 函数的参数类型
type FooParameters = Parameters<typeof bar>; // [n: number]
console.log('------------------------------------');
// 使用infer关键字,自定义封装一个ReturnType,获取函数的返回值类型
/**
* (...args: any[]) => any 代表一个函数类型
* T extends (...args: any[]) => any 代表泛型T必须是一个函数类型,且函数的参数类型和返回值类型是什么不确定,所以用T代替 => 限制T的范围
* (...args: any[]) => infer R 代表函数的返回值类型是什么不确定,所以用R代替
*/
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R
? R
: never;
type FooReturnType2 = MyReturnType<typeof bar>; // string
console.log('------------------------------------');
// 使用infer关键字,自定义封装一个Parameters,获取函数的参数类型
/**
* (...args: any[]) => any 代表一个函数类型
* T extends (...args: any[]) => any 代表泛型T必须是一个函数类型,且函数的参数类型和返回值类型是什么不确定,所以用T代替 => 限制T的范围
* (...args: infer P) => any 代表函数的参数类型是什么不确定,所以用P代替
*/
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any
? P
: never;
type FooParameters2 = MyParameters<typeof bar>; // [n: number]
分发条件类型
当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的
很多内置工具底层都是用了分发,很重要!!!
type toArray<T> = T extends any ? T[] : never;
type NumberArray = toArray<number>; // number[]
type StringArray = toArray<string>; // string[]
/**
* number[] | string[] 而不是 (number | string)[]
* 因为extxtends是分发式的, 会把联合类型拆分成多个条件, 然后再进行条件判断, 最后再合并成一个联合类型
* 1. 当传入string | number时,会遍历联合类型中的每一个成员
* 2. 相当于执行了ToArray<string> | ToArray<number>
* 3. 最后合并成一个联合类型,也就是string[] | number[]
*/
type NumberOrStringArray = toArray<number | string>; // number[] | string[]
十、TypeScript知识扩展
1. TypeScript模块化
主要有两种 :
commonjs => node环境 => webpack/vite
module.exports = {}
ESmodule => 在TypeScript中最主要使用的模块化方案就是ES Module
import / export 语法
非模块

内置类型导入

util/math.ts
export function sum(num1: number, num2: number): number {
return num1 + num2;
}
util/math.ts
export type Idtype = string | number;
export interface IPersion {
name: string;
age: number;
}
main.ts
// 导入普通的js模块,正常使用即可
import { sum } from './util/math';
console.log(sum(1, 2)); // 3
/**
* 当导入类型时,可以使用type关键字
* 因为最终编译后的代码中,不会有ts的类型代码
*
* 让编译器自己识别的话,会比较耗性能,因为要多一步类型的判断
* 所以用type声明后,方便编译打包时的删除,减少代码量,提高性能
*/
import { type Idtype, type IPersion } from './util/type';
// 如果导入的全都是类型,可以使用import type
// import type { Idtype, IPersion } from './util/type';
const id: Idtype = 123;
const person: IPersion = {
name: 'why',
age: 18
};
2. TypeScript的命名空间
了解即可,直接使用ES模块

// 定义命名空间
namespace time {
// 只有导出的函数才能被外部调用
export function format(): number {
return new Date().getTime();
}
function foo(): string {
return '123';
}
const bar = '123';
// 只有导出的变量才能被外部调用
export const baz = '456';
}
// 调用命名空间导出的函数
console.log(time.format()); // 1702541990130
// 调用命名空间导出的变量
console.log(time.baz); // 456
// 导出命名空间,可以被其他模块使用
export namespace price {
export function format(): string {
return '123.456';
}
}
// 其他模块
// import { price } from './文件名.ts';
3. 类型的查找

内置类型声明
概念
内置类型声明是typescript自带的、帮助我们内置了JavaScript运行时的一些标准化API的声明文件
内置类型声明通常在我们安装typescript的环境中会带有的
- 包括比如Function、String、Math、Date等内置类型
- 也包括运行环境中的DOM API,比如Window、Document等
TypeScript 使用模式命名这些声明文件lib.[something].d.ts

配置
可在 tsconfig.json 的 target以及l ib 中进行配置内置声明

外部定义类型声明
外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明
外部定义类型声明 通常有两种类型声明的方式
方式一
在自己的库中进行类型声明 ( 编写.d.ts文件 )
例如 : axios,可在node_modules中找到

方式二
通过社区的一个 公有库DefinitelyTyped 存放类型声明文件
例如 : react,在node_modules中找不到

DefinitelyTyped的github地址 : GitHub - DefinitelyTyped
( 推荐 ) 查找声明安装地址的方式 : TypeScript: Search for typed packages

自己定义类型声明
假如第三方库中没有声明文件,需要自己编写.d.ts文件~
只要有代码的实现,声明一下,在代码中即可运行,这里使用webpack配置的环境
ps : 不需主动引入,会自动查找

步骤一
在index.html中写代码的实现
ps :
按道理来说,这里是最顶层,这里定义后,其他的文件中都能使用,但是不行
因为其他的打包后会到bound.js中,然后在这里被引用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
let starName = 123
function foo(){
console.log('foo');
}
</script>
</body>
</html>
步骤二
在src目录下生成star.d.ts文件
需要生声明后,变成全局后,才能在其他ts中使用
// 声明变量
declare const starName: string;
// 声明函数
declare function foo(): void;
// 如果用到了图片,需要声明下,要不然解析不了, 因为图片是一个文件, 所以不能直接在声明文件中引用
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.svg';
declare module '*.bmp';
declare module '*.tiff';
// 声明vue文件
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
// 声明命名空间,是全局的,可通过Star.xxx访问
declare namespace Star {
export const coderName: string;
}
步骤三
在main.js中进行调用
// 如果没有在.d.ts文件中声明,是访问不到的
console.log(starName); // 123
foo(); // foo
4. declare
declare 声明模块

declare 声明文件

declare 声明命名空间

5. tsconfig.json
JavaScript 项目可以使用 jsconfig.json 文件,它的作用与 tsconfig.json 基本相同,只是默认启用了一些 JavaScript 相关的编译选项
作用
tsconfig.json文件有两个作用:
- 作用一(主要的作用):
- 让TypeScript Compiler在编译的时候,知道如何去编译TypeScript代码和进行类型检测
- 比如是否允许不明确的this选项( noImplicitThis ),是否允许隐式的any类型 ( noImplicitAny )
- 将TypeScript代码编译成什么版本的JavaScript代码 ( target )
- 作用二:
- 让编辑器(比如VSCode)可以按照正确的方式识别TypeScript代码
- 对于哪些语法进行提示、类型错误检测等等
使用
手动使用
-
在调用 tsc 命令并且没有其它输入文件参数时,编译器将由当前目录开始向父级目录寻找包含 tsconfig 文件的目录

-
调用 tsc 命令并且没有其他输入文件参数,可以使用 --project (或者只是 -p)的命令行选项来指定包含了 tsconfig.json 的目录
- 当命令行中指定了输入文件参数, tsconfig.json 文件会被忽略
使用webpack
webpack中使用ts-loader进行打包时,也会自动读取tsconfig文件,根据配置编译TypeScript代码
选项
顶层选项

compilerOptions选项

十一、内置工具和类型体操

1. Partial<Type>
用于构造一个Type下面的所有属性都设置为可选的类型
官方 - 内置工具
interface Ikun {
name: string;
age: number;
say?: () => void;
}
/**
* Ikun都变成可选的
* Partial 的作用就是将某个类型里的属性全部变为可选项 ?
* 结果如下:
* interface Ikun {
* name?: string | undefined;
* age?: number | undefined;
* say?: () => void | undefined;
* }
*/
type IkunOptional = Partial<Ikun>;
类型体操 - 实现
interface Ikun {
name: string;
age: number;
say?: () => void;
}
/**
* 使用映射类型实现Partial
* 将某个类型里的属性全部变为可选项 ?
*/
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
/**
* 结果如下:
* type IkunOptional = {
* name?: string | undefined;
* age?: number | undefined;
* say?: () => void | undefined;
* }
*/
type IkunOptional = MyPartial<Ikun>;
2. Required<Type>
用于构造一个Type下面的所有属性全都设置为必填的类型,这个工具类型跟 Partial 相反
官方 - 内置工具
interface Ikun {
name?: string;
age: number;
say?: () => void;
}
/**
* Ikun都变成必填的
* Required 的作用就是将某个类型里的属性全部变为可选项 ?
* 结果如下:
* interface Ikun {
* name: string;
* age: number;
* say: () => void;
* }
*/
type IkunOptional = Required<Ikun>;
类型体操 - 实现
interface Ikun {
name?: string;
age: number;
say?: () => void;
}
/**
* 使用映射类型实现Required
* 将某个类型里的属性全部变为必传属性
*/
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
/**
* 结果如下:
* type IkunRequired = {
* name: string;
* age: number;
* say: () => void;
* }
*/
type IkunOptional = MyRequired<Ikun>;
3. Readonly<Type>
用于构造一个Type下面的所有属性全都设置为只读的类型,意味着这个类型的所有的属性全都不可以重新赋值
官方 - 内置工具
interface Ikun {
name?: string;
age: number;
say: () => void;
}
/**
* Ikun都变成只读的
* Readonly 会将所有属性变成只读的,也就是不可修改
* 结果如下:
* interface Ikun {
* readonly name?: string | undefined;
* readonly age: number;
* readonly say: () => void;
* }
*/
type IkunOptional = Readonly<Ikun>;
类型体操 - 实现
interface Ikun {
name?: string;
age: number;
say: () => void;
}
/**
* 使用映射类型实现readonly
* 将某个类型里的属性变成只读的
*/
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
/**
* 结果如下:
* interface Ikun {
* readonly name?: string | undefined;
* readonly age: number;
* readonly say: () => void;
* }
*/
type IkunOptional = MyReadonly<Ikun>;
4. Record<Keys, Type>
用于构造一个对象类型,它所有的key(键)都是Keys类型,它所有的value(值)都是Type类型
官方 - 内置工具
type IAddresskeys = '上海' | '北京' | '广州' | '深圳';
interface Ikun {
name: string;
age: number;
say?: () => void;
}
/**
* Record<K,T> 用于将 K 中所有的属性的值转化为 T 类型
* 结果的类型为一个新的对象类型,该对象包含了 K 中所有的属性,每个属性值都是 T 类型
* 结果如下:
* type IBlack = {
* 上海: Ikun;
* 北京: Ikun;
* 广州: Ikun;
* 深圳: Ikun;
* }
*/
type IBlack = Record<IAddresskeys, Ikun>;
const black: IBlack = {
上海: { name: '小红', age: 18 },
北京: { name: '小蓝', age: 18 },
广州: { name: '小紫', age: 18 },
深圳: {
name: '小黑子',
age: 18,
say() {
console.log('深圳');
}
}
};
类型体操 - 实现
type IAddresskeys = '上海' | '北京' | '广州' | '深圳';
interface Ikun {
name: string;
age: number;
say?: () => void;
}
/**
* Keys 必须 是联合类型,且联合类型的值必须是 'string' | 'number' | 'symbol' 的子集
* 1. keyof any => 'string' | 'number' | 'symbol',代表的是所有可以作为属性名的类型
* 2. Keys extends keyof any => 代表的是 Keys 必须是 'string' | 'number' | 'symbol' 的子集
* 3. [P in Keys]: T => 代表的是遍历 Keys,将 Keys 中的属性名作为新对象的属性名,属性值为 T
*/
type MyRecord<Keys extends keyof any, T> = {
[P in Keys]: T;
};
/**
* 结果如下:
* type IBlack = {
* 上海: Ikun;
* 北京: Ikun;
* 广州: Ikun;
* 深圳: Ikun;
* }
*/
type IBlack = MyRecord<IAddresskeys, Ikun>;
const black: IBlack = {
上海: { name: '小红', age: 18 },
北京: { name: '小蓝', age: 18 },
广州: { name: '小紫', age: 18 },
深圳: {
name: '小黑子',
age: 18,
say() {
console.log('深圳');
}
}
};
5. Pick<Type, Keys>
用于构造一个类型,它是从Type类型里面挑了一些属性Keys
官方 - 内置工具
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* Pick 从某个类型中挑选出指定的属性
* 结果如下:
* type IkunPick = {
* name: string;
* address?: string | undefined;
* }
*/
type IkunPick = Pick<Ikun, 'name' | 'address'>;
类型体操 - 实现
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* K extends keyof T 表示 K 是 T 的属性 => 约束 K 的类型
* [P in K]: [P 是 K 的属性]
*/
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
/**
* 结果如下:
* type IkunPick = {
* name: string;
* address?: string | undefined;
* }
*/
type IkunPick = MyPick<Ikun, 'name' | 'address'>;
6. Omit<Type, Keys>
用于构造一个类型,它是从Type类型里面过滤了一些属性Keys
官方 - 内置工具
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* Omit<T, K> 表示忽略 T 中的 K 属性
* 获取 Ikun 中除了 name 属性之外的所有属性
* 结果如下:
* type IkunOmit = {
* age: number;
* say?: (() => void) | undefined;
* address?: string | undefined;
* }
*/
type IkunOmit = Omit<Ikun, 'name'>;
类型体操 - 实现
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* 从T中排除K
* 使用了分发条件类型
* P in keyof T => 表示遍历T中的所有属性
* as P => 表示将遍历的结果赋值给P
* P extends K ? never : P => 表示如果P是K的子集,则返回never,否则返回P
*/
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
/**
* 结果如下:
* type IkunOmit = {
* age: number;
* address?: string | undefined;
* }
*/
type IkunOmit = MyOmit<Ikun, 'name' | 'say'>;
7. Exclude<UnionType, ExcludedMembers>
用于构造一个类型,它是从UnionType联合类型里面排除了所有可以赋给ExcludedMembers的类型
官方 - 内置工具
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* Exclude<UnionType, ExcludedMembers>
* 从UnionType中排除ExcludedMembers
* UnionType: 联合类型
* ExcludedMembers: 要排除的成员, 可以是单个成员,也可以是多个成员组成的联合类型
*
* 结果如下:
* type IkunExclude = "address" | "say"
*/
type IkunExclude = Exclude<keyof Ikun, 'name' | 'age'>;
可以使用它来实现MyOmit
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* 从T中排除U
* Exclude<keyof T, U> => keyof T中排除U,把交集排除掉
* [K in Exclude<keyof T, U>]: T[K] => 从T中排除U后,再遍历T中的key,然后取出T中的value
*/
type MyOmit<T, U extends keyof T> = {
[K in Exclude<keyof T, U>]: T[K];
}
/**
* 结果如下:
* type IkunExclude = "address" | "say"
*/
type IkunExclude = MyOmit<Ikun, 'name' | 'age'>;
类型体操 - 实现
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* 从T中排除U
* 使用了分发条件类型
* T extends U => 表示如果T是U的子集,那么返回never,否则返回T
*/
type MyExclude<T, U> = T extends U ? never : T;
/**
* 结果如下:
* type IkunExclude = "address" | "say"
*/
type IkunExclude = MyExclude<keyof Ikun, 'name' | 'age'>;
8. Extract<Type, Union>
用于构造一个类型,它是从Type类型里面提取了所有可以赋给Union的类型,和Exclude相反
官方 - 内置工具
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* Extract<Type, Union>
* 从Type中提取出可以赋值给Union的类型
* Type: 联合类型
* Union: 要提取的类型,单个类型或者联合类型
* 结果如下:
* type IkunExclude = "name" | "age"
*/
type IkunExtract = Extract<keyof Ikun, 'name' | 'age'>;
类型体操 - 实现
interface Ikun {
name: string;
age: number;
say?: () => void;
address?: string;
}
/**
* 方式一
* @param T => Ikun
* @param U => 'name' | 'age'
* K in keyof T => k是Ikun的key, 也就是name, age, say, address
* as K => 表示将遍历的结果赋值给K
* K extends U ? K : never => 如果K是U的子集,那么返回K, 否则返回never
*/
type MyExtract<T, U extends keyof T> = {
[K in keyof T as K extends U ? K : never]: T[K];
};
/**
* 结果如下
* type IkunExtract{
* name: string;
* age: number;
* }
*/
type IkunExtract = MyExtract<Ikun, 'name' | 'age'>;
console.log('------------------------------------');
/**
* 方式二
* @param T => Ikun的所有key, 也就是name | age | say | address
* @param U => 'name' | 'age'
* T extends U ? T : never => 如果T是U的子集,那么返回T, 否则返回never
*/
type MyExtract2<T, U> = T extends U ? T : never;
/**
* 结果如下
* type IkunExtract{
* name: string;
* age: number;
* }
*/
type IkunExtract2 = MyExtract2<keyof Ikun, 'name' | 'age'>;
9. NonNullable<Type>
用于构造一个类型,这个类型从Type中排除了所有的null、undefined的类型
官方 - 内置工具
type IKun = string | number | undefined | null | boolean;
/**
* NonNullable
* 从类型T中排除null和undefined
* 结果如下
* type Ikuns = string | number | boolean
*/
type Ikuns = NonNullable<IKun>;
类型体操 - 实现
type IKun = string | number | undefined | null | boolean;
/**
* T extends null | undefined => 如果T是null或者undefined,返回never,否则返回T
*/
type MyNonNullable<T> = T extends null | undefined ? never : T;
/**
* 结果如下
* type Ikuns = string | number | boolean
*/
type Ikuns = MyNonNullable<IKun>;
10. InstanceType<Type>
用于构造一个由所有Type的构造函数的实例类型组成的类型
官方 - 内置工具
class Person {}
const p1: Person = new Person();
/**
* typeof Person => Person构造函数的类型
* InstanceType<typeof Person> => Person构造函数构建出的实例的类型
*/
type MyPerson = InstanceType<typeof Person>;
const p2: MyPerson = new Person();
类型体操 - 实现
class Person {}
class Studen {}
/**
* 可以使用infer关键字来推断出函数的返回值类型
* 推断出p的类型为Person
* 推断出s的类型为Studen
* 在返回值处重新写一遍<T extends new (...args: any[]) => any>
* 是为了让函数的返回值类型和传入的构造函数类型保持一致
* 如果不写的话,会推断出p和s的类型为any
*/
function fectory<T extends new (...args: any[]) => any>(
ctor: T
): T extends new (...args: any[]) => infer R ? R : any {
return new ctor();
}
const p = fectory(Person);
const s = fectory(Studen);
console.log('-------------------');
/**
* 可以使用InstanceType<T>来获取构造函数的实例类型
* 推断出p2的类型为Person
* 推断出s2的类型为Studen
*/
function fectory2<T extends new (...args: any[]) => any>(ctor: T): InstanceType<T> {
return new ctor();
}
const p2 = fectory2(Person);
const s2 = fectory2(Studen);
深入TypeScript:强化JavaScript类型系统
本文探讨了TypeScript作为JavaScript的超集,如何通过类型约束增强代码安全性,介绍其安装、配置、变量声明、类型推导、常见数据类型及高级特性如泛型、接口等,指导开发者高效利用TypeScript提升前端开发质量。
3万+

被折叠的 条评论
为什么被折叠?



