零基础学习 之 TS

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

零基础学习 之 TS_第1张图片

一、认识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 文件名

零基础学习 之 TS_第2张图片

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模版

零基础学习 之 TS_第3张图片

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  =>  进行类型推导,推导出来的是字面量类型

零基础学习 之 TS_第4张图片


四、TypeScript的数据类型

零基础学习 之 TS_第5张图片

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 = []  

// 写法二 : 推荐
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');

// 
// 用 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);

原因

零基础学习 之 TS_第6张图片

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; // number
// 参数类型
type calsParameters = Parameters; // [n1: number, n2: number]

function bar(str: string): string {
  return 'foo';
}
// 返回值类型
type FooReturnType = ReturnType; // string
// 参数类型
type FooParameters = Parameters; // [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,注释可能没用

零基础学习 之 TS_第7张图片

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; // { 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; // (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 指定 this 的类型为 IState
 * 使用交叉类型将 this 的类型和 store 的类型合并
 * 该对象中的所有方法中的 this 都会被推断为 IState
 * 
 * 相当于不用在每个方法中都写 this: IState!
 */
const store: IStore & ThisType = {
  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. 类的成员修饰符 

零基础学习 之 TS_第8张图片

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. 对象类型的属性修饰符

零基础学习 之 TS_第9张图片

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. 抽象类和接口的区别 

零基础学习 之 TS_第10张图片


九、TypeScript的泛型

零基础学习 之 TS_第11张图片

1. 泛型的基本使用

  • 在定义函数时,不决定参数的类型
  • 是让程序员决定参数的类型,通过传入不同的参数来决定
  • 格式: function 函数名(参数:T):T{}
  • :传递的类型参数
  • T相当于一个变量,用于记录传入的参数的类型,在整个函数执行过程中,T的类型是不变的
// 封装一个函数,传入一个参数,并且返回这个参数

function sum(n: T): T {
  return n;
}

/**
 * 完整的写法
 * 调用方式一 : 明确指定参数类型
 * 在调用函数时,用明确的类型来代替T
 */
const n1 = sum(1); // n1的类型是number
const n2 = sum('str'); // n2的类型是string
const n3 = sum([true, false]);
const n4 = sum([
  [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(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. 泛型接收多个参数 

零基础学习 之 TS_第12张图片

function foo(arg1: T, arg2: E, arg3: O): [T, E, O] {
  return [arg1, arg2, arg3];
}

// 1. 指定类型
const [a, b, c] = foo([1, 2], { name: 'why' }, true);

// 2. 类型推断
const res = foo(1, 'str', true); // [1, 'str', true] => [number, string, boolean]

4. 泛型接口 

/**
 * 泛型
 * T: 类型变量
 * O:类型变量,默认值为string
 */
interface Iperson {
  name: T;
  age: O;
  address: string;
}

// 传递类型变量,T为string,O为number,使用默认值
const p: Iperson = {
  name: '张三',
  age: 18,
  address: '北京'
};

// 传递类型变量,T为number,O为string
const p1: Iperson = {
  name: 11,
  age: '22',
  address: '北京'
};

5. 泛型类

class point {
  x: T;
  y: T;
  constructor(x: T, y: T) {
    this.x = x;
    this.y = y;
  }
}

// 1. 指定泛型类型
const p1 = new point(1, 2);
const p3: point = new point('1.11', '1.22');

// 2. 类型推断
const p2 = new point('1.11', '1.22');

// 3. 指定数组有两种,一种是元素类型后面加[], 一种是Array<元素类型>
const names1: string[] = ['Max', 'Manu'];
const names2: Array = ['Max', 'Manu'];

6. 泛型的类型约束

例子一 : 简单约束

interface Ilength {
  length: number;
}

// 规定传入的参数必须包含length属性
function getLength(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(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进行映射
 *  2.3 keyof Type => 表示Type中的所有key组成的联合类型 => "name" | "age"
 *  2.4 [K in keyof Type] => 表示对Type中的每一个key进行映射
 */
type MapToPerson = {
  [K in keyof Type]: Type[K];
};

// 3. 使用映射类型,将IPerson中的所有属性都映射到新的类型中
type newPerson = MapToPerson; // {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 = {
  readonly [k in keyof T]?: T[k];
};

/**
 *  更改后的结果: 都变成了只读的可选属性
 *  readonly name?: string | undefined;
 *  readonly age?: number | undefined;
 *  readonly address?: string | undefined;
 */
type newPerson = MapToPerson; 

还可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀 

interface IPerson {
  name: string;
  age?: number;
  readonly height: number;
  address?: string;
}

/**
 * -? : 删除属性的可选性
 * -readonly : 删除属性的只读性
 */
type MapToPerson = {
  -readonly [k in keyof T]-?: T[k];
};

/**
 *  删除后的结果: 所有属性都是必选的
 *  name: string;
 *  age: number;
 *  height: number;
 *  address: string;
 */
type PersonRequire = MapToPerson;

8. 条件类型

零基础学习 之 TS_第13张图片

基本使用

/**
 * 条件类型例子一
 */
// 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(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 : 在返回值处重新写一遍 any>

零基础学习 之 TS_第14张图片

/**
 * 使用infer关键字
 * 自定义封装一个ReturnType,获取函数的返回值类型
 * 自定义封装一个Parameters,获取函数的参数类型
 */

// 使用TypeScript内置的ReturnType和Parameters
function bar(n: number): string {
  return `${n}hello`;
}
// 函数的返回值类型
type FooReturnType = ReturnType; // string
// 函数的参数类型
type FooParameters = Parameters; // [n: number]

console.log('------------------------------------');

// 使用infer关键字,自定义封装一个ReturnType,获取函数的返回值类型
/**
 *  (...args: any[]) => any 代表一个函数类型
 *  T extends (...args: any[]) => any 代表泛型T必须是一个函数类型,且函数的参数类型和返回值类型是什么不确定,所以用T代替 => 限制T的范围
 * (...args: any[]) => infer R 代表函数的返回值类型是什么不确定,所以用R代替
 */
type MyReturnType any> = T extends (...args: any[]) => infer R
  ? R
  : never;
type FooReturnType2 = MyReturnType; // string

console.log('------------------------------------');

// 使用infer关键字,自定义封装一个Parameters,获取函数的参数类型
/**
 * (...args: any[]) => any 代表一个函数类型
 * T extends (...args: any[]) => any 代表泛型T必须是一个函数类型,且函数的参数类型和返回值类型是什么不确定,所以用T代替 => 限制T的范围
 * (...args: infer P) => any 代表函数的参数类型是什么不确定,所以用P代替
 */
type MyParameters any> = T extends (...args: infer P) => any
  ? P
  : never;

type FooParameters2 = MyParameters; // [n: number]

分发条件类型

当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的

很多内置工具底层都是用了分发,很重要!!!

type toArray = T extends any ? T[] : never;

type NumberArray = toArray; // number[]
type StringArray = toArray; // string[]

/**
 * number[] | string[] 而不是 (number | string)[]
 * 因为extxtends是分发式的, 会把联合类型拆分成多个条件, 然后再进行条件判断, 最后再合并成一个联合类型
 *  1. 当传入string | number时,会遍历联合类型中的每一个成员
 *  2. 相当于执行了ToArray | ToArray
 *  3. 最后合并成一个联合类型,也就是string[] | number[]
 */
type NumberOrStringArray = toArray; // number[] | string[]

十、TypeScript知识扩展

1. TypeScript模块化

主要有两种 :

commonjs => node环境 => webpack/vite

module.exports = {}

ESmodule => 在TypeScript中最主要使用的模块化方案就是ES Module

import / export 语法

非模块

零基础学习 之 TS_第15张图片

内置类型导入 

零基础学习 之 TS_第16张图片

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模块

零基础学习 之 TS_第17张图片

// 定义命名空间
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. 类型的查找

零基础学习 之 TS_第18张图片

内置类型声明

概念

内置类型声明是typescript自带的、帮助我们内置了JavaScript运行时的一些标准化API的声明文件

内置类型声明通常在我们安装typescript的环境中会带有的

  • 包括比如Function、String、Math、Date等内置类型
  • 也包括运行环境中的DOM API,比如Window、Document等

TypeScript 使用模式命名这些声明文件lib.[something].d.ts 

零基础学习 之 TS_第19张图片

配置

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

零基础学习 之 TS_第20张图片

外部定义类型声明

外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明

外部定义类型声明 通常有两种类型声明的方式

方式一

在自己的库中进行类型声明 ( 编写.d.ts文件 )

例如 : axios,可在node_modules中找到

零基础学习 之 TS_第21张图片

方式二

通过社区的一个 公有库DefinitelyTyped 存放类型声明文件

例如 : react,在node_modules中找不到

零基础学习 之 TS_第22张图片

DefinitelyTyped的github地址 : GitHub - DefinitelyTyped

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

零基础学习 之 TS_第23张图片

自己定义类型声明 

假如第三方库中没有声明文件,需要自己编写.d.ts文件~

只要有代码的实现,声明一下,在代码中即可运行,这里使用webpack配置的环境

ps : 不需主动引入,会自动查找

零基础学习 之 TS_第24张图片

步骤一

在index.html中写代码的实现

ps :

按道理来说,这里是最顶层,这里定义后,其他的文件中都能使用,但是不行

因为其他的打包后会到bound.js中,然后在这里被引用




  
  
  
  Document


  

步骤二

在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 声明模块

零基础学习 之 TS_第25张图片

declare 声明文件

零基础学习 之 TS_第26张图片

declare 声明命名空间

零基础学习 之 TS_第27张图片

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 文件的目录

零基础学习 之 TS_第28张图片

  • 调用 tsc 命令并且没有其他输入文件参数,可以使用 --project (或者只是 -p)的命令行选项来指定包含了 tsconfig.json 的目录

  • 当命令行中指定了输入文件参数, tsconfig.json 文件会被忽略
使用webpack

webpack中使用ts-loader进行打包时,也会自动读取tsconfig文件,根据配置编译TypeScript代码

选项

顶层选项

零基础学习 之 TS_第29张图片

compilerOptions选项

零基础学习 之 TS_第30张图片


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

零基础学习 之 TS_第31张图片

 练习       练习+答案

1. Partial

用于构造一个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;

类型体操 - 实现 

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
}

/**
 * 使用映射类型实现Partial
 * 将某个类型里的属性全部变为可选项 ?
 */
type MyPartial = {
  [P in keyof T]?: T[P];
};

/**
 * 结果如下:
 * type IkunOptional = {
 *  name?: string | undefined;
 *  age?: number | undefined;
 *  say?: () => void | undefined;
 * }
 */
type IkunOptional = MyPartial;

2. Required

用于构造一个Type下面的所有属性全都设置为必填的类型,这个工具类型跟 Partial 相反 

官方 - 内置工具

interface Ikun {
  name?: string;
  age: number;
  say?: () => void;
}

/**
 * Ikun都变成必填的
 * Required 的作用就是将某个类型里的属性全部变为可选项 ?
 * 结果如下:
 * interface Ikun {
 *  name: string;
 *  age: number;
 *  say: () => void;
 * }
 */
type IkunOptional = Required;

类型体操 - 实现 

interface Ikun {
  name?: string;
  age: number;
  say?: () => void;
}

/**
 * 使用映射类型实现Required
 * 将某个类型里的属性全部变为必传属性
 */
type MyRequired = {
  [P in keyof T]-?: T[P];
};

/**
 * 结果如下:
 * type IkunRequired = {
 *  name: string;
 *  age: number;
 *  say: () => void;
 * }
 */
type IkunOptional = MyRequired;

3. Readonly

用于构造一个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;

类型体操 - 实现

interface Ikun {
  name?: string;
  age: number;
  say: () => void;
}

/**
 * 使用映射类型实现readonly
 * 将某个类型里的属性变成只读的
 */
type MyReadonly = {
  readonly [P in keyof T]: T[P];
};
/**
 * 结果如下:
 * interface Ikun {
 *  readonly name?: string | undefined;
 *  readonly age: number;
 *  readonly say: () => void;
 * }
 */
type IkunOptional = MyReadonly;

4. Record

用于构造一个对象类型,它所有的key(键)都是Keys类型,它所有的value(值)都是Type类型

官方 - 内置工具

type IAddresskeys = '上海' | '北京' | '广州' | '深圳';
interface Ikun {
  name: string;
  age: number;
  say?: () => void;
}

/**
 * Record 用于将 K 中所有的属性的值转化为 T 类型
 * 结果的类型为一个新的对象类型,该对象包含了 K 中所有的属性,每个属性值都是 T 类型
 * 结果如下:
 * type IBlack = {
 *  上海: Ikun;
 *  北京: Ikun;
 *  广州: Ikun;
 *  深圳: Ikun;
 * }
 */
type IBlack = Record;

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 = {
  [P in Keys]: T;
};
/**
 * 结果如下:
 * type IBlack = {
 *  上海: Ikun;
 *  北京: Ikun;
 *  广州: Ikun;
 *  深圳: Ikun;
 * }
 */
type IBlack = MyRecord;

const black: IBlack = {
  上海: { name: '小红', age: 18 },
  北京: { name: '小蓝', age: 18 },
  广州: { name: '小紫', age: 18 },
  深圳: {
    name: '小黑子',
    age: 18,
    say() {
      console.log('深圳');
    }
  }
};

5. Pick

用于构造一个类型,它是从Type类型里面挑了一些属性Keys

官方 - 内置工具

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * Pick 从某个类型中挑选出指定的属性
 * 结果如下:
 * type IkunPick = {
 *  name: string;
 *  address?: string | undefined;
 * }
 */
type IkunPick = Pick;

类型体操 - 实现

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * K extends keyof T 表示 K 是 T 的属性 => 约束 K 的类型
 * [P in K]: [P 是 K 的属性]
 */
type MyPick = {
  [P in K]: T[P];
};

/**
 * 结果如下:
 * type IkunPick = {
 *  name: string;
 *  address?: string | undefined;
 * }
 */
type IkunPick = MyPick;

6. Omit 

用于构造一个类型,它是从Type类型里面过滤了一些属性Keys

官方 - 内置工具

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * Omit 表示忽略 T 中的 K 属性
 * 获取 Ikun 中除了 name 属性之外的所有属性
 * 结果如下:
 * type IkunOmit = {
 *  age: number;
 *  say?: (() => void) | undefined;
 *  address?: string | undefined;
 * }
 */
type IkunOmit = Omit;

类型体操 - 实现

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 = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

/**
 * 结果如下:
 * type IkunOmit = {
 *  age: number;
 *  address?: string | undefined;
 * }
 */
type IkunOmit = MyOmit;

7. Exclude 

用于构造一个类型,它是从UnionType联合类型里面排除了所有可以赋给ExcludedMembers的类型

官方 - 内置工具 

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * Exclude
 * 从UnionType中排除ExcludedMembers
 *  UnionType: 联合类型
 *  ExcludedMembers: 要排除的成员, 可以是单个成员,也可以是多个成员组成的联合类型
 *
 * 结果如下:
 * type IkunExclude = "address" | "say"
 */
type IkunExclude = Exclude;

可以使用它来实现MyOmit

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * 从T中排除U
 * Exclude => keyof T中排除U,把交集排除掉
 * [K in Exclude]: T[K] => 从T中排除U后,再遍历T中的key,然后取出T中的value
 */
type MyOmit = {
  [K in Exclude]: T[K];
}

/**
 * 结果如下:
 * type IkunExclude = "address" | "say"
 */
type IkunExclude = MyOmit;

类型体操 - 实现

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * 从T中排除U
 * 使用了分发条件类型
 * T extends U => 表示如果T是U的子集,那么返回never,否则返回T
 */
type MyExclude = T extends U ? never : T;

/**
 * 结果如下:
 * type IkunExclude = "address" | "say"
 */
type IkunExclude = MyExclude;

8. Extract 

用于构造一个类型,它是从Type类型里面提取了所有可以赋给Union的类型,和Exclude相反

官方 - 内置工具

interface Ikun {
  name: string;
  age: number;
  say?: () => void;
  address?: string;
}

/**
 * Extract
 * 从Type中提取出可以赋值给Union的类型
 *  Type: 联合类型
 *  Union: 要提取的类型,单个类型或者联合类型
 * 结果如下:
 * type IkunExclude = "name" | "age"
 */
type IkunExtract = Extract;

类型体操 - 实现

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 = {
  [K in keyof T as K extends U ? K : never]: T[K];
};
/**
 * 结果如下
 * type IkunExtract{
 *  name: string;
 *  age: number;
 * }
 */
type IkunExtract = MyExtract;

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 extends U ? T : never;
/**
 * 结果如下
 * type IkunExtract{
 *  name: string;
 *  age: number;
 * }
 */
type IkunExtract2 = MyExtract2;

9. NonNullable

 用于构造一个类型,这个类型从Type中排除了所有的null、undefined的类型

官方 - 内置工具

type IKun = string | number | undefined | null | boolean;

/**
 * NonNullable
 * 从类型T中排除null和undefined
 * 结果如下
 * type Ikuns = string | number | boolean
 */
type Ikuns = NonNullable;

类型体操 - 实现

type IKun = string | number | undefined | null | boolean;

/**
 * T extends null | undefined => 如果T是null或者undefined,返回never,否则返回T
 */
type MyNonNullable = T extends null | undefined ? never : T;

/**
 * 结果如下
 * type Ikuns = string | number | boolean
 */
type Ikuns = MyNonNullable;

10. InstanceType 

用于构造一个由所有Type的构造函数的实例类型组成的类型

官方 - 内置工具

class Person {}

const p1: Person = new Person();

/**
 * typeof Person => Person构造函数的类型 
 * InstanceType => Person构造函数构建出的实例的类型
 */
type MyPerson = InstanceType;
const p2: MyPerson = new Person();

类型体操 - 实现

class Person {}
class Studen {}

/**
 * 可以使用infer关键字来推断出函数的返回值类型
 * 推断出p的类型为Person
 * 推断出s的类型为Studen
 * 在返回值处重新写一遍 any>
 *    是为了让函数的返回值类型和传入的构造函数类型保持一致
 *    如果不写的话,会推断出p和s的类型为any
 */
function fectory 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来获取构造函数的实例类型
 * 推断出p2的类型为Person
 * 推断出s2的类型为Studen
 */
function fectory2 any>(ctor: T): InstanceType {
  return new ctor();
}
const p2 = fectory2(Person);
const s2 = fectory2(Studen);

你可能感兴趣的:(TS,学习,typescript,javascript)