typescript学习笔记

https://gitee.com/samuelsue/typescript-learning-notes

配置ts运行环境

需要安装的依赖

1
2
3
4
5
6
7
8
9
10
11
12
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.1.0",
"clean-webpack-plugin": "^3.0.0",
"eslint": "^7.2.0",
"html-webpack-plugin": "^4.3.0",
"ts-loader": "^7.0.5",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
entry: './src/index.ts',
output: {
filename: 'main.js',
path: path.resolve(__dirname,'../dist')
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
},
module: {
rules: [
{
oneOf:[
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
]
},
devtool: process.env.NODE_ENV === 'development'?'eval-source-map':'cheap-module-source-map',
plugins: [
new HtmlWebpackPlugin({
template: './src/template/index.html'
}),
new CleanWebpackPlugin()
],
devServer: {
contentBase: '../dist',
compress: true,
port: 8090,
open: true
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// eslintrc.js
module.exports = {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project":"./tsconfig.json",
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"extends":[
"plugin:@typescript-eslint/recommended"
],
"plugins": [
"@typescript-eslint"
],
"rules": {
},
// webstorm老是会去lint这俩文件,然后报错,所以加入忽略
"ignorePatterns": [
"webpack.config.js",
".eslintrc.js"
]
};
1
tsc --init # 初始化tsconfig.json

package.json添加脚本命令

1
2
3
4
5
"scripts": {
"dev": "webpack-dev-server --mode=development --config ./build/webpack.config.js",
"build": "webpack --mode=production --config ./build/webpack.config.js",
"lint": "eslint src --ext .js,.ts --fix"
},

1. 基本类型

  • boolean

  • number(支持二进制0b1101,八进制0o173,十六进制0x7b)

  • string

  • 数组:

    • number[], string[],......
    • Array<number'>....
    • 联合类型 (string|number)[]
  • 元组类型

    1
    let tuple:[string,number,boolean] = ['a',1,false] 长度和元素类型顺序都有要求
  • 枚举类型

    1
    2
    3
    4
    5
    6
    7
    enum Users {
    CREATOR, // 0
    ADMIN, // 1
    USER = 3 // 3 可以自行分配
    }
    console.log(Users.CREATOR)
    console.log(Users[3]) // USER
  • any类型

  • void类型 (undefined,在非strict模式下可以是null)

    1
    2
    3
    4
    5
    const consoleText = (txt:string):void => {
    console.log(txt)
    // 因为没有返回值,声明返回类型为void
    }
    consoleText('123')
  • undefined

  • null

  • never类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
    throw new Error(message);
    }

    // 推断的返回值类型为never
    function fail() {
    return error("Something failed");
    }

    // 返回never的函数必须存在无法达到的终点
    function infiniteLoop(): never {
    while (true) {
    }
    }

    const neverVal = ():never => {
    throw new Error('abc')
    }

    myStr = neverVal() // never类型可以赋值给任意类型,但是反过来不可以
  • object(eslint不推荐使用)

    1
    2
    3
    4
    5
    6
    function printObj(obj:object):void{
    console.log(obj)
    }
    printObj({
    name: 'Samuel'
    })

类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 类型断言
const getLength = (target: (string | number)): number => {
/*
在这个if里面我们知道target就是string类型而不是number,但是ts编译器不知道
所以, 需要类型断言告诉编译器这里的target是string类型的
*/
if ((<string>target).length || (target as string).length === 0) {
return (<string>target).length
} else {
return target.toString().length
}
}

getLength(123)

类型断言有两种写法

  • (<类型>变量)
  • (变量 as 类型) React只能这么写,因为<>会被当成标签

2 Symbol

Symbols是不可改变且唯一的。主要用法是用来做对象的属性名,这样可以保证属性名独一无二不被mixin覆盖

1
2
3
4
5
6
7
8
9
10
11
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

类型转换

Symbol不可以和其他类型的值做运算,但是有toString方法,且可以转换成布尔

1
2
3
4
5
6
7
8
9
10
11
12
let sym = Symbol('My symbol');

"your symbol is " + sym
// TypeError: can't convert symbol to string
1 + sym
// TypeError

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

Boolean(sym) // true
!sym // false

用处

Symbol可以用来解决强耦合的某一个具体的字符串或数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function getArea(shape, options) {
let area = 0;

switch (shape) {
case 'Triangle': // 魔术字符串(强耦合)
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}

return area;
}

// 可以定义一个对象来指定case类型
const shapeType = {
triangle: 'Triangle'
};

function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle: // 解耦
area = .5 * options.width * options.height;
break;
}
return area;
}

// 其实triangle是什么字符串不重要,因此可以使用Symbol
const shapeType = {
triangle: Symbol()
};

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

1
2
3
4
5
6
7
8
9
10
const nameSymbol = Symbol('name')
const obj = {
[nameSymbol]: 'Samuel',
age:18
}
for(const key in obj) {
console.log(key) // age
}
console.log(Object.keys(obj)) // Array["age"]
console.log(Object.getOwnPropertyNames(obj)) // Array["age"]

但是可以被Object.getOwnPropertySymbols()Reflect.ownKeys()获取

1
console.log(Object.getOwnPropertySymbols(obj)) // Array [ Symbol(name) ]

Symbol.for(),Symbol.keyFor()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。

1
2
3
4
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.keyFor()会返回一个使用Symbol.for(key)登记过的key

1
2
3
4
5
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

内置Symbol值

Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

1
2
3
4
5
6
7
8
// es6  js, 非ts
class MyClass {
[Symbol.hasInstance](foo) {
console.log(foo)
}
}

[1, 2, 3] instanceof new MyClass()

Symbol.isConcatSpreadable

对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

1
2
3
4
5
6
7
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']

Symbol.species

对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

1
2
3
4
5
6
7
8
9
class MyArray extends Array {
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true

子类MyArray继承了父类ArrayaMyArray的实例,bca的衍生对象。你可能会认为,bc都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例。

1
2
3
4
5
6
7
8
9
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}

const a = new MyArray();
const b = a.map(x => x);

b instanceof MyArray // false
b instanceof Array // true

上面代码中,a.map(x => x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

有关字符串的3个Symbol

  • Symbol.match
  • Symbol.replace
  • Symbol.split
1
2
3
String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)

Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

1
2
3
4
5
6
7
8
9
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]
myIterable.next()

Symbol.toPrimitive

当对象转换为基本类型时调用这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};

2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'

Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。这个属性可以用来定制Object.prototype.toString.call(obj)的返回的[object Object][object Array]object后面的那个字符串。

1
2
3
4
5
6
7
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"

3. 接口

限制对象属性

使用接口限制对象的属性和属性类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用接口限制属性类型
interface NameInterface {
firstName: string,
lastName: string
}

// 限制对象作为参数传递时属性的类型
function getFullname({firstName, lastName}: NameInterface): string {
return `${firstName}·${lastName}`
}

console.log(getFullname({
firstName: "Samuel",
lastName: 'Sue'
}))

可选属性

可选属性的限制

1
2
3
4
5
6
7
8
9
10
11
// 对于可选参数接口
interface VegetableInterface {
color?: string, // 可以有color也可以没有
type: string
}

function VegInfo({color, type}: VegetableInterface): string {
return `${color || ''}${type}`
}

console.log(VegInfo({type: 'Onion'}))

多余属性

对于多余属性,有三种方法解除属性限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
enum Gender {
Female,
Male,
}

// 对于多余属性
interface PersonInfo {
name: string,
age: number,
gender: Gender,

// 多余属性解决方法1, 定义其他属性
[props: string]: any
}

function logPerson({name, age, gender}: PersonInfo): void {
console.log(`Person:${name},${age} years old,${gender}`)
}

logPerson({
name: 'Samuel',
age: 25,
gender: Gender.Male,
hobby:'Basketball' // 可以传递多余属性
})

// ==========================================
logPerson(({
name: 'Samuel',
age: 25,
gender: Gender.Male,
hobby:'Basketball' // 可以传递多余属性
}as PersonInfo)) // 多余属性解决方法2,类型断言
// ===========================================
// 多余属性解决方法3 类型兼容性
const person = {
name: 'Samuel',
age: 25,
gender: Gender.Male,
hobby:'Basketball' // 可以传递多余属性
}
logPerson(person)

注意,剩余属性(属性索引)是规定了其他属性的类型的,举个例子:

1
2
3
4
interface Role{
[prop:string]:number, // 这就规定了Role接口描述的对象的所有属性的值都必须是number
// name: string, // 这就报错了,因为name属性的值是string类型的,没法赋值给number类型.
}

只读属性

1
2
3
4
5
6
7
8
// 只读属性
interface ArrInterface {
0: number, // 0位置只能是number
readonly 1: string // 1位置只读,不可改
}

const arr:ArrInterface = [123,'hahah']
// arr[1] = 'good' // Cannot assign to '1' because it is a read-only property.

属性类型

1
2
3
4
5
6
7
8
9
10
// 属性名如果是number类型,会自动转换成string
interface RoleDict {
[id:string]:number
}

const role:RoleDict = {
12:123, // 12实际上是转换成了string
// '12':234 // 报 属性重复 的错误
}
console.log(role['12'])

接口继承

1
2
3
4
// 接口继承
interface Tomato extends VegetableInterface{
radius: number
}

函数接口

函数接口: 注意函数类型写法(arg:string,arg2:number):void,即(参数:类型...):返回值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 计数器函数接口
interface CounterInterface {
(): void, // 函数类型属性, 参数为空,无返回值
counter: number
}

const countFn = ():CounterInterface => {
const fn = function () {
fn.counter++
}
fn.counter = 0
return fn
}

const countFunc:CounterInterface = countFn()
countFunc()
console.log(countFunc.counter)
countFunc()
console.log(countFunc.counter)

4. 函数

函数类型

1
2
3
4
5
6
7
// 接口来定义函数类型(lint推荐使用类型别名来定义)
interface AddFnInterface {
(x: number,y: number):number // 函数类型属性
}
// 类型别名定义函数类型
type Add = (x: number, y: number) => number
const addFunc: Add = (x: number, y: number) => x + y

可选参数,默认参数

1
2
3
// 函数可选参数,默认参数
type AddFnType = (x: number, y?: number) => number
const fn2: AddFnType = (x = 3, y) => x + (y || 0)

剩余参数

1
2
3
4
// 剩余参数
type SumFnType = (...args: number[]) => number
const fn3: SumFnType = (...args) => args.reduce((previousValue, currentValue) => previousValue + currentValue, 0)
console.log(fn3(1, 2, 3, 4, 5, 6))

函数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数重载(ts的重载只是用于代码提示,不是真正意义上的重载)
function splitChar(x: number): string[]
function splitChar(x: string): string[]
function splitChar(x: any): any { // 前两个是方法声明(支持重载类型), 这里才是方法实体(必须这么写)
if (typeof x === 'string') {
return x.split('')
} else {
return (x as number).toString().split('')
}
}

console.log(splitChar(123456))
console.log(splitChar('我是爸爸'))

5. 泛型

对于函数,传入的参数类型事前无法确定,在调用时确定,且返回值类型也需要根据参数类型确定,同时对于这种动态的写法我们有时仍需要对类型做判断以调用不同类型对象的方法。

这时候我们需要的就是泛型

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// (函数)变量类型泛型
const genArr = <T>(val: T, times: number): T[] => {
return new Array(times).fill(val)
}

// 函数类型别名泛型
type GenArr = <T>(val: T, times: number) => T[]
const genFn: GenArr = (val, times) => {
return new Array(times).fill(val)
}

// genArr(123,3).map(item=>item.length) // Property 'length' does not exist on type 'number'.
genArr(123,3).map(item=>item.toFixed())
console.log(genArr('爸爸',3).map(item=>item.split(''))) // 根据val类型, 可以提示响应类型的方法

泛型约束(结合接口)

1
2
3
4
5
6
7
8
9
10
// 泛型约束, 假如只允许带length属性的类型传入
interface WithLength {
length: number
}
const genFn2 = <T extends WithLength>(val: T, times: number): T[] => {
return new Array(times).fill(val)
}
genFn2([1,2,3],2)
genFn2('爸爸',2)
// genFn2(123,2) //Argument of type '123' is not assignable to parameter of type 'WithLength

理解泛型中的keyof

http://semlinker.com/ts-keyof/

https://juejin.cn/post/6844903826558812167

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
interface WithLength {
length: number
}

// 这里T extends WithLength, 理解上就是泛型类型T=WithLength(type T = WithLength)
const genFn2 = <T extends WithLength>(val: T, times: number): T[] => {
return new Array(times).fill(val)
}

interface Person{
name: string;
age: number;
gender: string;
}

class Teacher{
constructor(private info:Person){}
/*
keyof Person就是:'name','age','gender',
T extends 'name':等价与 type T = 'name'
...
T类型就是属性名的联合类型。
所以这里参数key就是属性名。 Person[T]就是属性值类型
*/
getInfo<T extends keyof Person>(key: T):Person[T] {
return this.info[T];
}
}
// keyof Person 其返回类型是联合类
// type K1 = keyof Person;
// "name" | "age" | "gender"
// type K2 = keyof Person[];
// number | "length" | "push" | "concat" | ...
// type K3 = keyof { [x: string]: Person };
// string | number

// keyof 操作符除了支持接口和类之外,它也支持基本数据类型:
// let K1: keyof boolean;
// let K1: "valueOf"
// let K2: keyof number;
// let K2: "toString" | "toFixed" | "toExponential" | ...
// let K3: keyof symbol;
// let K1: "valueOf"

const teacher = new Teacher({
name: 'sssssss',
age: 18,
gender: 'male'
})

const test = teacher.getInfo('age');
console.log(test);

typeof语法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const COLORS = {
red: 'red',
blue: 'blue'
}

// 首先通过typeof操作符获取color变量的类型,然后通过keyof操作符获取该类型的所有键,
// 即字符串字面量联合类型 'red' | 'blue'

type Colors = typeof COLORS
let color: Colors;
color = 'red' // Ok
color = 'blue' // Ok

// Type '"yellow"' is not assignable to type '"red" | "blue"'.
color = 'yellow' // Error

泛型约束(结合索引类型)

1
2
3
4
5
6
7
8
9
10
11
12
// 泛型约束, 只允许访问对象存在的属性
function showProp<T, K extends keyof T>(obj: T, prop: K) {
return obj[prop]
}

const person1 = {
name: 'Samuel',
age: 26,
gender: 'Male'
}
console.log(showProp(person1,"age"))
// console.log(showProp(person1,"addr")) //Argument of type '"addr"' is not assignable to parameter of type '"age" | "name" | "gender"'.

6. 类

es6的类

补充知识点:

  • set/get关键字操作属性值
  • 私有化方法/属性(不是语法上的添加_,真正意义上的私有化)
  • new.target
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Parent{
constuctor(){
if(new.target===Parent){
throw new Error('父类不允许实例化')
}
}
}
let privateSymbol = new Symbol('name')
class Child extends Parent{
constructor(name){
super()
this[privateSymbol] = name
console.log(new.target)
}
getName(){
return this[privateSymbol]
}
}
export Child // 只导出Child类,不导出privateSymbol,外部无法直接访问

// new Parent // throw Error
new Child() // 打印Child的构造方法(也就是这个类)
  • super关键字的用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Parent{
constructor(){
this.name = 'Parent'
}
showName(){
return this.name
}
static getName(){
return Parent.name
}
}

class Child extends Parent{
constructor(){
super() // 指向父类构造方法
this.name = 'Child'
}
showParentName(){
return super.showName() // 成员方法中super指向父类实例(但是this是指向自己的)
}
static getParentName(){
return super.getName() // 静态方法中super指向父类静态类
}
}

Child.getParentName() // 'Parent'
new Child().getParentName() // 'Parent' super指向父类实例(但是this是指向自己的)

TS中的类

属性修饰符

  • public
  • private(仅自身可用)
  • protected(仅自身和派生类[子类]可用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Parent {
protected x: number

// 构造方法声明protect, 只能在派生类中被super执行
protected constructor(x: number) {
this.x = x
}

protected getX() {
return this.x
}
}

class Child extends Parent {
constructor(x: number) {
super(x); //
}

getX(): number {
// console.log(super.x) //子类内部只能通过super访问父类的protect方法,不能访问protect属性
return super.getX();
}
}

// const p = new Parent(123)
const ch = new Child(234)
console.log(ch.getX())

只读属性

1
2
3
4
5
6
7
8
9
10
11
12
// 只读属性
class ReadOnlyInfo {
public readonly info: string

constructor(info: string) {
this.info = info
}
}

const rInfo = new ReadOnlyInfo('只能读,不能改')
// rInfo.info = '改一个试试' // Cannot assign to 'info' because it is a read-only property.
console.log(rInfo)

参数修饰符

1
2
3
4
5
6
7
8
9
// 参数修饰符(简写)
class A {
constructor(private name: string) {
// 使用参数修饰符可以自动帮你把参数绑定到this上
}
}

const a = new A('Samuel')
console.log(a) // {name:'Samuel'}

静态成员属性

1
2
3
4
5
6
7
8
9
// 静态成员属性
class B {
public static readonly pi = 3.1415926
private static num = 2333
}

console.log(B.pi)
// B.pi = 3.14
// console.log(B.num)

可选属性/参数, 存取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 可选属性/可选参数  set/get
class Human {
private _name: string
public age?: number

constructor(name: string, age?: number, public gender?: string) {
this._name = name
this.age = age
}

public set name(newVal: string) {
this._name = newVal
}

public get name() {
return this._name.replace(/./, substring => substring.toUpperCase())
}
}

const h = new Human("samuel")
h.name = 'zhangboy'
console.log(h.name)

抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 抽象类
abstract class MyFile {
protected constructor(public size: number) {
}

public abstract save(): void // 抽象函数,没有函数体
}


class MediaFile extends MyFile {
constructor(public size: number, public type: string) {
super(size) // 必须执行父类(抽象类)构造函数
}

save(): void {
console.log('save media file')
}

play(): void {
console.log('play media File')
}
}

类实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 类实现接口
interface player {
supportType: string[],
play: () => void
}

class MusicPlayer implements player {
play(): void {
console.log('play music')
}

supportType: string[];

constructor(...types: string[]) {
this.supportType = types
}
}

const player = new MusicPlayer('mp3', 'wav', 'wma', 'acc')
player.play()

接口继承类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接口继承类
class Point {
protected x: number; // 因为是protect,因此只能在内部或者派生类中初始化
protected y: number;

protected constructor(x: number, y: number) {
this.x = x
this.y = y
}
}

interface Point3d extends Point {
z: number;
}

// 因为要初始化protected x,y 所以要继承类
class AcPoint extends Point implements Point3d {
z: number;

constructor(x: number, y: number, z: number) {
super(x, y)
this.z = 312
}
}

类类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
假如我们要限制参数是一个具体类的构造函数,此时就需要使用类类型
*/
class TestE {
public info: string

constructor() {
console.log('constructor exec')
this.info = 'hhh'
}
}

// c的类型是对象类型且这个对象包含返回类型是T的构造函数。
// 理解c是一个对象(构造函数),执行new之后返回一个类型为T的对象
const testFn1 = <T>(c: new() => T): T => {
return new c()
}

testFn1(TestE)

7. 枚举

枚举值编号

1
2
3
4
5
6
7
8
9
10
const f = 400

enum Code {
OK,
Fault = f, // 可以用const或者函数返回值来编号
Error = 3, // 紧接着的一个枚举值要手动编号
Success
}

console.log(Code[400]) // 可以通过编号获取枚举值

字符串枚举

1
2
3
4
5
6
// 字符串枚举
enum reply {
Error = 'Sorry, an error has occurred',
Success = 'No problem',
Failed = Error, // 可以引用枚举
}

异构枚举(不推荐)

1
2
3
4
5
// 异构枚举, 枚举值类型不同(不推荐)
enum XX {
a = 1,
b = 'hehe',
}

枚举成员类型和联合枚举类型

如果一个枚举里所有成员的值都是字面量类型的值,那么这个枚举的每个成员和枚举本身都可以作为类型来使用。字面量枚举成员需满足以下条件:

  • 不带初始值的枚举成员,例如 enum E { A }
  • 值为字符串字面量,例如 enum E { A = 'hello' }
  • 值为数字字面量,或者带有一元 - 符号的数字字面量,例如 enum E { A = 1 },enum E { A = -1 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 枚举成员类型
enum ShapeKind {
Circle,
Square
}
interface Circle {
kind: ShapeKind.Circle // 使用 ShapeKind.Circle 作为类型,指定接口须有 kind 字段,且类型为 ShapeKind.Circle
radius: number
}
let c: Circle = {
kind: ShapeKind.Square, // Error! 因为接口 Circle 的 kind 被指定为 ShapeKind.Circle类型,所以这里会报错
radius: 100
}

// 联合枚举
enum Status {
on,
off
}
interface Light {
status: Status // 只能是Status中的枚举值
}
const light:Light = {
status: Status.off,
}

const枚举

普通的枚举在编译后会实实在在创建一个对象(枚举名同名),然后让变量指向对象的值(枚举值),使用const枚举后不会创建这个对象,而是直接用

1
2
3
4
5
6
7
8
// const enum
const enum Animal{
Dog,
Cat,
GoldFish
}

const kitty = Animal.Cat // 编译后是kitty=1, 不会产生Animal对象

8. 类型推断和类型兼容

类型推断

  • ts会自动推断写的代码是什么类型
  • 当你不需要ts自动推断类型的时候 需要用到类型断言
  • 最佳通过类型推断
  • 上下文类型推断
1
2
3
4
// 断言为number
let a = 1;
// 断言为number[]
let b = [1,2]

类型兼容

  • 当一个类型Y可以被赋值给另外一个类型X时,我们就可以说类型X兼容类型Y
  • 结构之间兼容,成员少的兼容成员多的
  • 函数之间兼容,参数多的兼容参数少的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 类型兼容
let fn1 = (x: string, y: string): void => {
console.log(11)
}
let fn12 = (x: string): void => {
console.log(22)
}

// fn12 = fn1 // 函数,只能参数多的兼容少的
fn1 = fn12

interface Aitf {
name:string
}
interface Bitf {
name:string,
age:number
}
let aObj : Aitf = {name:'Samuel'}
let bObj : Bitf = {name:'Samuel',age:25}
// bObj = aObj // 结构数据,只能成员少的兼容多的
aObj = bObj
  • 函数重载类型兼容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 重载类型兼容
function shk(x: number, y: number): number
function shk(x: string, y: string): string
function shk(x: any, y: any): any {
return x + y
}

function shl(x: number, y: number): number
function shl(x: any, y: any): any {
return x + y
}

let fna = shl
fna = shk // 重载类型少的兼容多的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 类兼容性
class AC{
// public static num = 3.14 // 静态成员不参与兼容性检测
protected num:number
private age:number
constructor(public name:string,age:number) {
this.num = 13
this.age = age
}
}

class ACsub extends AC{
constructor(public name:string,age:number) {
super(name,age);
this.num = 15
}
}

class BC{
constructor(public name:string) {
}
}

let ax:BC = new BC('name') // 结构成员
ax = new AC('hh',17) // 少的兼容多的
ax = new ACsub('hh',25) // protected以及private成员必须保证来自同一个类

console.log(ax)

9. 高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。 可以理解成逻辑与

1
2
3
4
5
const mergeObj = <T, U>(a: T, b: U): T & U => {
let res = {} as T & U // 类型断言
res = Object.assign(a, b)
return res
}

联合类型

可以理解成

1
2
3
4
const showRes = (): string | number => {
const arr = ['Samuel', 123]
return Math.random() > .5 ? arr[0] : arr[1]
}

注意一点,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Bird {
fly():void
layEggs():void
}
interface Turtle {
swim():void
layEggs():void
}

function getSmallPet ():Bird|Turtle {
return {} as Bird|Turtle
}

getSmallPet().layEggs() // 返回的联合类型只能访问其类型的共有属性
// getSmallPet().swim() // 各自的非共有属性无法访问

类型保护

如果我们想确定变量的类型,比如是不是string类型或者其他类型,以往我们需要做大量的类型断言,ts提供类型保护机制,可以方便使用

1
2
3
4
5
6
7
8
9
10
function isString(x: string | number): x is string {
return typeof x === 'string'
}

const res1 = showRes() // 联合类型 string | number
if (isString(res1)) {
console.log('string length:', res1.length)
} else {
console.log('number:', res1.toFixed())
}

typeof做简单的类型保护

typeof能判断的类型有:string/number/boolean/symbol/undefined/function,其他都是object

ts中用typeof做类型保护只能判断string/number/boolean/symbol,其他类型不会识别为类型保护

1
2
3
4
5
6
// 对于简单的类型保护,可以直接使用typeof(只能做===或!==判断)
if (typeof res1 === 'string') {
console.log('string length:', res1.length)
} else {
console.log('number:', res1.toFixed())
}

intanceof做类型保护

判断是否是特定类创建的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Axx {
public age = 13
}

class Bxx {
public name = 'Sam'
}

function getRandomAB() {
return Math.random() > .5 ? new Axx() : new Bxx()
}

const rsAB = getRandomAB()
if (rsAB instanceof Axx) {
console.log(rsAB.age)
} else {
console.log(rsAB.name)
}

null/undefined类型断言

1
2
3
4
5
6
7
8
function getPrefixStr(x:number|null) {
function addPrefix(prefix:string):string {
// 因为下面已经对null情况进行排除,这里使用'!'类型断言x不为null/undefined
return prefix + x!.toFixed().toString()
}
x = x || 0.1
return addPrefix('pr')
}

类型别名

类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Container<T> = { value: T };

type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

type可以给抽象类取别名,但是注意别名不能使用extends,implement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class AbsC{
protected name:string
protected constructor() {
this.name = 'Samuel'
}
}

type absc = AbsC // 不能Extends
class CCC extends AbsC{
constructor() {
super();
}
}

字面量类型

1
2
3
4
5
6
7
8
9
// 字符串字面量类型
type Direction = 'east'|'west'|'south'|'north'
const DirFn = (direc:Direction) => {
console.log(direc)
}
DirFn('west')
// DirFn('hollyshit') // Argument of type '"hollyshit"' is not assignable to parameter of type 'Direction'.

// 同理还有数字字面量类型

可辨识联合

  1. 具有普通的单例类型属性 —可辨识的特征
  2. 一个类型别名包含了那些类型的联合—联合
  3. 此属性上的类型保护。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}

type Shape = Square | Rectangle | Circle; // 声明为联合类型

function area(s: Shape) {
// 类型保护
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}

索引类型

keyof

返回索引属性名构成的联合类型

1
2
3
4
5
6
interface ACtp2 {
name: string,
age: number
}

let k: keyof ACtp2 // 返回索引属性名构成的联合类型 "name" | "age"

索引访问操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// --------------------------------------------------- T[K]是索引访问操作符
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}

interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

在普通的上下文里使用T[K]需要保证K extends keyof T

使用[]意味着取出属性(值或类型)

1
2
3
4
5
6
7
8
// [] 取出接口类型
interface Types {
a: number,
b: string,
c: boolean,
}

type AllTypes = Types[keyof Types] // number | string | boolean

映射类型

简单例子:将一个类型的所有属性都变成可读

1
2
3
type ReadOnlyTypes<T> = {
readonly [P in keyof T]: T[P] // in 表示遍历
}

内置的映射类型:

  • Readonly 只读

  • Partial 可选

  • Pick<T,K> T里面保留K(可以是联合类型)属性

  • Record<K extends keyof any, T> 创建一个类型,K代表键值的类型, T代表值的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Record实例 将属性值映射为属性string的长度
    function record<K extends string | number, T, U>(obj: Record<K, T>, fn: (x: T) => U): Record<K, U> {
    const res: any = {}
    for (const key in obj) {
    res[key] = fn(obj[key])
    }
    return res
    }

    console.log(record(person2, (p) => p.toString().length))
  • Omit<T,K> 实现排除已选的属性

    1
    2
    3
    4
    5
    6
    type User = {
    id: string,
    name: string,
    email: string
    }
    type UserWithoutEmail = Omit<User, 'email'>; // alias for '{id:string,name:string}'
  • Exclude<T,U> 从T中提出可以赋值给U的类型

  • Extract<T, U> -- 提取T中可以赋值给U的类型。

  • NonNullable<T> -- 从T中剔除nullundefined

  • ReturnType<T> -- 获取函数返回值类型。

  • InstanceType<T> -- 获取构造函数类型的实例类型。

可以显式的指定添加或着删除修饰符

1
2
3
4
5
6
// 显示添加/删除修饰符
type RemoveReadonly<K> = {
-readonly [P in keyof K]: K[P] // - 表示删除修饰符,可以是readonly,?(可选修饰符)
}

type noReadOnlyTypes = RemoveReadonly<readTypess>

unknown类型

any 和 unknown https://zhuanlan.zhihu.com/p/104296850

  1. 任何类型都可以赋值给unknown类型

  2. 在没有类型断言或者基于控制流的类型细化时,unknown不可以赋值给其他类型

  3. 在没有类型断言或者基于控制流的类型细化时,不可以对unknown类型对象进行任何操作

    1
    2
    let val2: unknown
    // val2++ // Object is of type 'unknown'.
  4. unknown与其他类型组成交叉类型, 就等于其他类型

  5. unknown与其他类型组成联合类型, 等于unknown

  6. never是unknown的子类型

  7. keyof unknown得到never

  8. unknown类型只能进行===或!==操作, 不能进行其他操作

  9. unknown类型不能访问属性,作为函数调用以及作为类创建对象

  10. 使用类型映射时如果映射的属性是unknown类型,就不会产生映射

    1
    2
    3
    4
    5
    6
    type NumsType<T> = {
    [P in keyof T]: number
    }

    type xxd = NumsType<any> // Initial type:{[p: string]: number}
    type xxs = NumsType<unknown> // Initial type:{}

条件类型

extends条件类型

1
2
3
type Typs<T> = T extends string ? string : number
let vals2: Typs<'2'> // let vals2: string
let vals3: Typs<false> // let vals3: number 走后面的逻辑

分布式条件类型

1
2
3
type onlyString<T> = (T extends string ? T : never)
type someTypes = 'a' | 'b' | 123 | false
type resultTypes = onlyString<someTypes> // Initial type: "a" | "b"

条件类型加索引类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type reserveFunction<T> = {
[P in keyof T]: T[P] extends () => void ? P : never // 取出所有是函数类型的属性P, 其他为never
}[keyof T] // 索引属性来去掉never类型属性
/*
不加这个索引属性,下面的Res2会变成:
type Res2 = {
user: never;
code: never;
subparts: never;
updateTime: "updateTime";
thankU: "thankU"; }
*/

interface Part {
user: 'Sam',
code: 132,
subparts: Part[]
updateTime:()=>void,
thankU(): void
}

type Res2 = reserveFunction<Part>
const obj2: Res2 = 'updateTime'

内置条件类型

  • Exclude<T,U> 从T里面排除U

    1
    type typesA = Exclude<'a'|'b'|'c', 'a'|'b'> // "c"
  • Extract<T,U> 从T中选取可以赋值给U的类型

    1
    type typesB = Extract<'a'|'b'|'c', 'a'|'b'> // "a" | "b"
  • NonNullable<T> 去掉null类型(undefined,null)

    1
    type typesC = NonNullable<'a' | 'b' | 'c' | null | undefined>  // Initial type:"a" | "b" | "c"
  • ReturnType<T> T的返回值类型

    1
    type typesD = ReturnType<() => string>  //Initial type:string
  • InstanceType<T> ts中类有2种类型, 静态部分的类型和实例的类型, 所以T如果是构造函数类型, 那么InstanceType可以返回他的实例类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class AClass{
    constructor() {
    console.log('AClass')
    }
    }

    interface AConstructor{
    new(): string
    }

    type typeE = InstanceType<typeof AClass> // AClass
    type typeF = InstanceType<AConstructor> // string
  • Parameters<T> 获取函数参数类型

    1
    2
    3
    4
    5
    interface A{
    (a:number, b:string):string[];
    }

    type A1 = Parameters<A> // [number, string]
  • ConstructorParameters 获取构造函数的参数类型

    1
    2
    3
    4
    5
    6
    7
    class AClass{
    constructor(public name:string) {
    console.log('AClass')
    }
    }

    type typeG = ConstructorParameters<typeof AClass> // string

推断元素类型

1
2
3
4
5
6
7
8
9
10
// 推断元素类型
type Type2<T> = T extends any[] ? T[number] : T // 如果T是一个数组, 那就取T元素的类型(当元素类型不一样时是联合类型)
type typesX = Type2<string[]> // Initial type:string
type typesX2 = Type2<boolean[]> // Initial type:boolean

// infer 推断元素类型
type Type3<T> = T extends Array<infer U> ? U : T
type typesY = Type3<string[]> // Initial type:string
type typesY2 = Type3<boolean[]> // Initial type:boolean
type typesY3 = Type3<[false,123,'sam']> // Initial type:false | 123 | "sam"
1
2
3
4
5
// T是一个函数类型(参数:any)返回值any, infer P推断参数类型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

// T是一个构造函数类型
type ConstructorParameters<T extends new(...arg: any) => any> = T extends new(...arg: infer P) => any ? P : never

10. 命名空间

为了防止全局变量过多,使用命名空间来管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// components.ts
namespace Components{
// 导出需要暴露的内容
export class Header {
constructor() {
const elem = document.createElment('div');
elem.innerText = 'this is header';
document.body.appendChild(elem);
}
}
export class Content{}
export class Footer{}
// 可以暴露接口
export interface User{
name:string
}
// 可以暴露命名空间
export namespace Sub{
export class Test{}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
///<reference path='./components.ts'>
/*
有一个组件命名空间,还有页面的命名空间
page.ts
*/
namespace Home{
export class Page {
constructor() {
new Components.Header();
new Components.Content();
new Components.Footer();
}
}
}

这个不太好用,好像没有挺多应用。

在给第三方库的描述文件中有很多引用。

11. 描述文件

针对全局的描述文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// jquery.d.ts

// 声明全局变量
declare var $: (param:()=>void) => void;

// 定义接口
interface JqueryInstance {
html: (html:string) => JqueryInstance
}

// 声明全局函数
declare function $(readyFunc: ()=>void):void
declare function $(selector:string):JqueryInstance

// 声明一个对象(有属性嵌套, new $.fn.init())
declare namespace $ { // 用namespace来
namespace fn {
class init{}
}
}

针对模块的描述文件

1
2
3
4
5
6
7
8
9
10
11
12
13
// jquery.d.ts

// 声明一个模块
declare module 'jquery' {
interface JqueryInstance {
html: (html:string) => JqueryInstance
}
declare function $(readyFunc: ()=>void):void
declare function $(selector:string):JqueryInstance

// 最终要导出出去
export = $;
}

12. 装饰器

装饰器本身就是一个函数

类装饰器

类装饰器接收的参数是构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function testDecorator (constructor:new (...args:any[])=>any) {
constructor.prototype.getName = () => {
console.log('dell')
}
console.log('decorator')
}

function testDecorator1 (constructor:any) {
console.log('decorator1')
}

@testDecorator1
@testDecorator
class Test {}
(new Test() as any).getName()

装饰器如果要支持接收参数,那么可以用闭包做装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 闭包作装饰器
function decor1 (flag:boolean) {
if (flag) {
return function (constructor:any) {
// 修改构造函数的原型(类的原型)
constructor.prototype.info = () => {
console.log('dell')
}
}
}
return function (constructor:any) {}
}
@decor1(true) // 接收参数
class Test1 {}

// 但是这么写,ts无法检测到原型上的info函数,因此无法推断出Test1类型的对象上面有info函数

如果类装饰器返回一个值,他会使用提供的构造函数来替换类的声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类装饰器泛型
function testDecor<T extends new (...args:any[])=>any>(constructor:T) {
return class extends constructor {
name = 'lee';
getName () {
console.log(this.name)
}
}
}

@testDecor
class Person {
name: string;
constructor (name:string) {
this.name = name
}
}
(new Person('sam') as any).getName() // 这种写法TS识别不到getName(), 改进方式是以下写法

装饰器工厂函数写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 装饰器工厂函数写法
function decoratorFactory () {
return function <T extends new (...args:any[])=>any>(constructor:T) {
return class extends constructor {
name = 'lee';
getName () {
console.log(this.name)
}
}
}
}

// 利用工厂函数对匿名类进行装饰
const Person1 = decoratorFactory()(class {
name: string;
constructor (name:string) {
this.name = name
}
})

new Person1('sam').getName() // 拓展的方法可以显示出来

方法装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 方法装饰器
function funcDecor (target: any, key:string, descriptor:PropertyDescriptor) {
/* target参数是方法对应的那个类的prototype
如果方法是静态方法,target是那个类的构造函数
*/
console.log(target)
console.log(key) // key是被装饰的方法名
console.log(descriptor) // js的属性描述
// descriptor.writable = false
// 直接修改方法
const oldVal = descriptor.value as (...args:any[])=> any
descriptor.value = function (...args:any[]) {
return oldVal.apply(this, args) + 'NO!!!!!!!!!!!'
}
}

class UObj {
name:string
constructor (name:string) {
this.name = name
}

// 方法装饰器
@funcDecor
getName () {
return this.name
}
}

const objU = new UObj('sam')
console.log(objU.getName())

访问器装饰器

注意: set和get不能同时绑定,目前用处不知道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function visitDecor (target:any, key:string, descriptor:PropertyDescriptor) {
console.log('descriptor:', descriptor)
}

class XObj {
private _name:string;
constructor (name:string) {
this._name = name
}

@visitDecor
get name () {
return this._name
}

// @visitDecor
set name (name:string) {
this._name = name
}
}

访问器装饰器和方法装饰器区别:

  1. 访问器装饰器的descriptor有set,get, 没有value和writable
  2. 方法装饰器没有set/get,有value和writable

属性装饰器

属性装饰器不接受descriptor参数,但是可以返回一个新的descriptor来覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 属性装饰器
function propDecorator (target:any, key:string):any {
// 创建新的属性描述符来覆盖(返回)
/* const descriptor:PropertyDescriptor = {
writable: false
}
return descriptor */
// 因为target是类对应原型,因此修改是原型上的属性,而不是实例上的属性
target[key] = 'NoNoNO'
}

class XaObj {
@propDecorator
name='Dell'
}
const xao = new XaObj()
xao.name = 'Samuel'
console.log(xao.name) // Samuel
console.log((xao as any).__proto__.name) // NoNoNO

参数装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*
* @param target 原型
* @param method 方法名
* @param paramIndex 参数的位置索引
*/
function paramDecorator (target: any, method: string, paramIndex:number) {
console.log(target, method, paramIndex)
}

class Sth {
getInfo (name:string, @paramDecorator age: number) {
console.log(name, age)
}
}

new Sth().getInfo('Sam', 30)

执行顺序

1
属性》方法》方法参数》类

结合Reflect-metadata

元数据可以理解为在任意对象上挂载了一个Map,用于绑定键值信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'reflect-metadata'

function Role (name:string):ClassDecorator {
// 类装饰器, target是构造函数
return target => {
Reflect.defineMetadata('role', name, target.prototype)
console.log(target)
}
}

@Role('admin')
class Post {}

const metaData = Reflect.getMetadata('role', Post.prototype)
console.log(metaData)
const np = new Post()
const anotherMd = Reflect.getMetadata('role', (np as any).__proto__)
console.log(anotherMd)

13. 问题

import type https://juejin.cn/post/6844904034952806414