[ 万字总结 ] 夯实你的 TypeScript 基础!(一)
1.环境配置和搭建
一.什么是Typescript
TypeScript
是Javascript
的超集,遵循最新的ES5/ES6
规范。Typescript
扩展了Javascript
语法。
- Typescript更像后端JAVA,让
JS
可以开发大型企业应用 - TS提供的类型系统可以帮助我们在写代码时提供丰富的语法提示
- 在编写代码时会对代码进行类型检查从而避免很多线上错误
TypeScript
不会取代JS
, 尤雨溪: 我认为将类型添加到JS
本身是一个漫长的过程 。让委员会设计一个类型系统是(根据TC39
的经历来判断)不切实际的 。
二.环境配置
1.全局编译TS文件
全局安装typescript
对TS
进行编译
npm install typescript -g
tsc --init # 生成tsconfig.json
tsc # 可以将ts文件编译成js文件
tsc --watch # 监控ts文件变化生成js文件
2.配置webpack
环境
-
安装依赖
npm install rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
-
初始化
TS
配置文件npx tsc --init
-
webpack
配置操作// rollup.config.js import ts from 'rollup-plugin-typescript2' import {nodeResolve} from '@rollup/plugin-node-resolve'; import serve from 'rollup-plugin-serve'; import path from 'path' export default { input:'src/index.ts', output:{ format:'iife', file:path.resolve('dist/bundle.js'), sourcemap:true }, plugins:[ nodeResolve({ extensions:['.js','.ts'] }), ts({ tsconfig:path.resolve(__dirname,'tsconfig.json') }), serve({ open:true, openPage:'/public/index.html', port:3000, contentBase:'' }) ] }
-
package.json
配置"scripts": { "dev": "rollup -c -w" }
我们可以通过
npm run start
启动服务来使用typescript啦~
2.基础类型
TS中冒号后面的都为类型标识
一.布尔、数字、字符串类型
let bool:boolean = true;
let num:number = 10;
let str:string = 'hello zf';
二.元组类型
限制长度个数、类型一一对应
let tuple:[string,number,boolean] = ['zf',10,true];
// 像元组中增加数据,只能增加元组中存放的类型
tuple.push('回龙观');
三.数组
声明数组中元素数据类型
let arr1:number[] = [1,2,3];
let arr2:string[] = ['1','2','3'];
let arr3:(number|string)[] = [1,'2',3];
let arr4:Array<number | string> = [1,'2',3]; // 泛型方式来声明
四.枚举类型
enum USER_ROLE {
USER, // 默认从0开始
ADMIN,
MANAGER
}
// {0: "USER", 1: "ADMIN", 2: "MANAGER", USER: 0, ADMIN: 1, MANAGER: 2}
可以枚举,也可以反举
// 编译后的结果
(function (USER_ROLE) {
USER_ROLE[USER_ROLE["USER"] = 0] = "USER";
USER_ROLE[USER_ROLE["ADMIN"] = 1] = "ADMIN";
USER_ROLE[USER_ROLE["MANAGER"] = 2] = "MANAGER";
})(USER_ROLE || (USER_ROLE = {}));
-
异构枚举
enum USER_ROLE { USER = 'user', ADMIN = 1, MANAGER, }
-
常量枚举
const enum USER_ROLE { USER, ADMIN, MANAGER, } console.log(USER_ROLE.USER)// console.log(0 /* USER */);
五.any类型
不进行类型检测
let arr:any = ['jiagou',true,{name:'zf'}]
六.null 和 undefined
任何类型的子类型,如果strictNullChecks
的值为true,则不能把null 和 undefined付给其他类型
let name:number | boolean;
name = null;
七.void类型
只能接受null,undefined。一般用于函数的返回值
let a:void;
a = undefined;
严格模式下不能将
null
赋予给void
八.never类型
任何类型的子类型,never代表不会出现的值。不能把其他类型赋值给never
function error(message: string): never {
throw new Error("err");
}
function loop(): never {
while (true) { }
}
function fn(x:number | string){
if(typeof x == 'number'){
}else if(typeof x === 'string'){
}else{
console.log(x); // never
}
}
九.Symbol类型
Symbol表示独一无二
const s1 = Symbol('key');
const s2 = Symbol('key');
console.log(s1 == s2); // false
十.BigInt
类型
const num1 = Number.MAX_SAFE_INTEGER + 1;
const num2 = Number.MAX_SAFE_INTEGER + 2;
console.log(num1 == num2)// true
let max: bigint = BigInt(Number.MAX_SAFE_INTEGER)
console.log(max + BigInt(1) === max + BigInt(2))
number
类型和bigInt
类型是不兼容的
十一.object对象类型
object
表示非原始类型
let create = (obj:object):void=>{}
create({});
create([]);
create(function(){})
3.类型推导
一.类型推导
-
声明变量没有赋予值时默认变量是
any
类型let name; // 类型为any name = 'wj' name = 10;
-
声明变量赋值时则以赋值类型为准
let name = 'wj'; // name被推导为字符串类型 name = 10;
二.包装对象
我们在使用基本数据类型时,调用基本数据类型上的方法,默认会将原始数据类型包装成对象类型
let bool1:boolean = true;
let bool2:boolean = Boolean(1);
let bool3:Boolean = new Boolean(2);
boolean是基本数据类型 , Boolean是他的封装类
三.联合类型
在使用联合类型时,没有赋值只能访问联合类型中共有的方法和属性
let name:string | number // 联合类型
console.log(name!.toString()); // 公共方法
name = 10;
console.log(name!.toFixed(2)); // number方法
name = 'zf';
console.log(name!.toLowerCase()); // 字符串方法
这里的!表示此值非空
let ele: HTMLElement | null = document.getElementById('#app');
ele!.style.color = 'red'; // 断定ele元素一定有值
四.类型断言
-
类型断言
let name: string | number; (name! as number).toFixed(2); // 强制 ((<number>name!).toFixed(2));
尽量使用第一种类型断言因为在react中第二种方式会被认为是
jsx
语法 -
双重断言
let name: string | boolean; ((name! as any) as string);
尽量不要使用双重断言,会破坏原有类型关系,断言为any是因为any类型可以被赋值给其他类型
五.字面量类型
type Direction = 'Up' | 'Down' | 'Left' | 'Right';
let direction:Direction = 'Down';
可以用字面量当做类型,同时也表明只能采用这几个值(限定值)。类似枚举。
4.函数类型
一.函数的两种声明方式
-
通过function关键字来进行声明
function sum(a: string, b: string):string { return a+b; } sum('a','b')
可以用来限制函数的参数和返回值类型
-
通过表达式方式声明
type Sum = (a1: string, b1: string) => string; let sum: Sum = (a: string, b: string) => { return a + b; };
二.可选参数
let sum = (a: string, b?: string):string => {
return a + b;
};
sum('a'); // 可选参数必须在其他参数的最后面
三.默认参数
let sum = (a: string, b: string = 'b'): string => {
return a + b;
};
sum('a'); // 默认参数必须在其他参数的最后面
四.剩余参数
const sum = (...args: string[]): string => {
return args.reduce((memo, current) => memo += current, '')
}
sum('a', 'b', 'c', 'd')
五.函数的重载
function toArray(value: number): number[]
function toArray(value: string): string[]
function toArray(value: number | string) {
if (typeof value == 'string') {
return value.split('');
} else {
return value.toString().split('').map(item => Number(item));
}
}
toArray(123); // 根据传入不同类型的数据 返回不同的结果
toArray('123');
5.类
一.TS中定义类
class Pointer{
x!:number; // 实例上的属性必须先声明
y!:number;
constructor(x:number,y?:number,...args:number[]){
this.x = x;
this.y = y as number;
}
}
let p = new Pointer(100,200);
实例上的属性需要先声明在使用,构造函数中的参数可以使用可选参数和剩余参数
二.类中的修饰符
-
public
修饰符(谁都可以访问到)class Animal { public name!: string; // 不写public默认也是公开的 public age!: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } class Cat extends Animal { constructor(name: string, age: number) { super(name, age); console.log(this.name,this.age); // 子类访问 } } let p = new Cat('Tom', 18); console.log(p.name,p.age); // 外层访问
class Animal { constructor(public name: string, public age: number) { this.name = name; this.age = age; } }
我们可以通过参数属性来简化父类中的代码
-
protected
修饰符 (自己和子类可以访问到)class Animal { constructor(protected name: string, protected age: number) { this.name = name; this.age = age; } } class Cat extends Animal { constructor(name: string, age: number) { super(name, age); console.log(this.name, this.age) } } let p = new Cat('Tom', 18); console.log(p.name,p.age);// 无法访问
-
private
修饰符 (除了自己都访问不到)class Animal { constructor(private name: string, private age: number) { this.name = name; this.age = age; } } class Cat extends Animal { constructor(name: string, age: number) { super(name, age); console.log(this.name, this.age); // 无法访问 } } let p = new Cat('Tom', 18); console.log(p.name,p.age);// 无法访问
-
readonly
修饰符 (仅读修饰符)class Animal { constructor(public readonly name: string, public age: number) { this.name = name; this.age = age; } changeName(name:string){ this.name = name; // 仅读属性只能在constructor中被赋值 } } class Cat extends Animal { constructor(name: string, age: number) { super(name, age); } } let p = new Cat('Tom', 18); p.changeName('Jerry');
三.静态属性和方法
class Animal {
static type = '哺乳动物'; // 静态属性
static getName() { // 静态方法
return '动物类';
}
private _name: string = 'Tom';
get name() { // 属性访问器
return this._name;
}
set name(name: string) {
this._name = name;
}
}
let animal = new Animal();
console.log(animal.name);
静态属性和静态方法是可以被子类所继承的
四.Super属性
class Animal {
say(message:string){
console.log(message);
}
static getType(){
return '动物'
}
}
class Cat extends Animal {
say(){ // 原型方法中的super指代的是父类的原型
super.say('猫猫叫');
}
static getType(){ // 静态方法中的super指代的是父类
return super.getType()
}
}
let cat = new Cat();
console.log(Cat.getType())
五.类的装饰器
1.装饰类
function addSay(target:any){
target.prototype.say = function(){console.log('say')}
}
@addSay
class Person {
say!:Function
}
let person = new Person
person.say();
装饰类可以给类扩展功能,需要开启
experimentalDecorators:true
2.装饰类中属性
function toUpperCase(target:any,key:string){
let value = target[key];
Object.defineProperty(target,key,{
get(){
return value.toUpperCase();
},
set(newValue){
value = newValue
}
})
}
function double(target: any, key: string) {
let value = target[key];
Object.defineProperty(target, key, {
get() {
return value * 2;
},
set(newValue) {value = newValue}
})
}
class Person {
@toUpperCase
name: string = 'JiangWen'
@double
static age: number = 10;
getName() {
return this.name;
}
}
let person = new Person();
console.log(person.getName(),Person.age)
装饰属性可以对属性的内容进行改写,装饰的是实例属性则target指向类的原型、装饰的是静态属性则target执行类本身~
3.装饰类中方法
function noEnum(target:any,key:string,descriptor:PropertyDescriptor){
console.log(descriptor)
descriptor.enumerable = false;
}
class Person {
@toUpperCase
name: string = 'JiangWen'
@double
static age: number = 10;
@noEnum
getName() {
return this.name;
}
}
let person = new Person();
console.log(person); // getName 不可枚举
4.装饰参数
function addPrefix(target:any,key:string,paramIndex:number){
console.log(target,key,paramIndex); // Person.prototype getName 0
}
class Person {
@toUpperCase
name: string = 'JiangWen'
@double
static age: number = 10;
prefix!:string
@noEnum
getName(@addPrefix prefix:string) {
return this.name;
}
}
六.抽象类
抽象类无法被实例化,只能被继承,抽象方法不能在抽象类中实现,只能在抽象类的具体子类中实现,而且必须实现。
abstract class Animal{
name!:string;
abstract speak():void
}
class Cat extends Animal {
speak(){
console.log('猫猫叫');
}
}
class Dog extends Animal{
speak():string{
console.log('汪汪叫');
return 'wangwang'
}
}
定义类型时
void
表示函数的返回值为空(不关心返回值类型,所有在定义函数时也不关心函数返回值类型)
6.接口
接口可以在面向对象编程中表示行为的抽象,也可以描述对象的形状。 接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。 (接口中不能含有具体的实现逻辑)
一.函数接口参数
const fullName = ({firstName,lastName}:{firstName:string,lastName:string}):string =>{
return firstName + lastName
}
我们可以约束函数中的参数,但是类型无法复用
interface IFullName {
firstName:string,
lastName:string
}
const fullName = ({firstName,lastName}:IFullName):string =>{
return firstName + lastName
}
我们可以通过接口进行描述
二.函数类型接口
interface IFullName {
firstName:string,
lastName:string
}
interface IFn {
(obj:IFullName):string
}
const fullName:IFn = ({firstName,lastName})=>{
return firstName + lastName
}
通过接口限制函数的参数类型和返回值类型
三.函数混合类型
interface ICounter {
(): number; // 限制函数类型
count: 0 // 限制函数上的属性
}
let fn: any = () => {
fn.count++;
return fn.count;
}
fn.count = 0;
let counter:ICounter = fn;
console.log(counter());
console.log(counter());
四.对象接口
对象接口可以用来描述对象的形状结构
interface IVegetables {
readonly color:string,
size:string
}
interface IVegetables{
age?:number,
taste:'sour'|'sweet'
}
const tomato:IVegetables = {
color:'red',
size:'10',
taste:'sour'
}
tomato.color = 'green'; // 仅读属性不能进行修改
?标识的属性为可选属性,
readOnly
标识的属性则不能修改。多个同名的接口会自动合并
const tomato:IVegetables = {
color:'red',
size:'10',
taste:'sour',
type:'蔬菜'
} as IVegetables; // 多余的属性可以使用类型断言
五.任意属性、可索引接口
interface Person {
name: string;
[key: string]: any
}
let p: Person = {
name: 'wj',
age: 10,
[Symbol()]:'回龙观'
}
任意属性可以对某一部分必填属性做限制,其余的可以随意增减
interface IArr {
[key: number]: any
}
let p: IArr = {
0:'1',1:'2',3:'3'
}
let arr:IArr = [1,'d','c'];
可索引接口可以用于标识数组
六.类接口
这里先来强调一下抽象类和接口的区别,抽象类中可以包含具体方法实现。接口中不能包含实现
interface Speakable {
name:string;
speak():void;
}
interface ChineseSpeakable{
speakChinese():void
}
class Speak implements Speakable,ChineseSpeakable{
name!:string
speak(){}
speakChinese(){}
}
一个类可以实现多个接口,在类中必须实现接口中的方法和属性
七.接口继承
interface Speakable {
speak():void
}
interface SpeakChinese extends Speakable{
speakChinese():void
}
class Speak implements SpeakChinese{
speakChinese(): void {
throw new Error("Method not implemented.");
}
speak(): void {
throw new Error("Method not implemented.");
}
}
八.构造函数类型
interface Clazz {
new (name:string):any
}
function createClass(target:Clazz,name:string){
return new target(name); // 传入的是一个构造函数
}
class Animal {
constructor(public name:string){
this.name = name;
}
}
let r = createClass(Animal,'Tom');
这里无法标识返回值类型
interface Clazz<T> {
new(name: string): T
}
function createClass<T>(target: Clazz<T>, name: string):T {
return new target(name)
}
class Animal {
constructor(public name: string) {
this.name = name;
}
}
let r = createClass(Animal, 'Tom');
new() 表示当前是一个构造函数类型,这里捎带使用了下泛型。 在使用
createClass
时动态传入类型。
7.泛型
一.指定函数参数类型
- 单个泛型
const getArray = <T>(times:number,val:T):T[]=>{
let result:T[] = [];
for(let i = 0; i<times;i++){
result.push(val);
}
return result;
}
getArray(3,3); // 3 => T => number
- 多个泛型
function swap<T, K>(tuple: [T, K]): [K, T] {
return [tuple[1], tuple[0]]
}
console.log(swap(['a','b']))
二.函数标注的方式
- 类型别名
type TArray = <T, K>(tuple: [T, K]) => [K, T];
const getArray:TArray = <T, K>(tuple: [T, K]): [K, T] => {
return [tuple[1], tuple[0]]
}
可以使用类型别名,但是类型别名不能被继承和实现。一般联合类型可以使用类型别名来声明
- 接口
interface IArray{
<T,K>(typle:[T,K]):[K,T]
}
const getArray:IArray = <T, K>(tuple: [T, K]): [K, T] => {
return [tuple[1], tuple[0]]
}
能使用interface尽量使用interface
三.泛型接口使用
interface ISum<T> { // 这里的T是使用接口的时候传入
<U>(a: T, b: T): U // 这里的U是调用函数的时候传入
}
let sum: ISum<number> = (a:number, b:number) => {
return 3 as any
}
四.默认泛型
interface T2<T=string>{
name:T
}
type T22 = T2;
let name1:T22 = {name:'zf'}
可以指定泛型的默认类型,方便使用
五.类中的泛型
- 创建实例时提供类型
class MyArray<T>{ // T => number
arr: T[] = [];
add(num: T) {
this.arr.push(num);
}
getMaxNum(): T {
let arr = this.arr
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
let current = arr[i];
current > max ? max = current : null
}
return max;
}
}
let myArr = new MyArray<number>();
myArr.add(3);
myArr.add(1);
myArr.add(2);
console.log(myArr.getMaxNum());
- 校验构造函数类型
const createClass = <T>(clazz: new(name:string,age:number)=>T):T =>{
return new clazz(name,age);
}
createClass<Person2>(Person2)
六.泛型约束
- 泛型必须包含某些属性
interface IWithLength {
length:number
}
function getLen<T extends IWithLength>(val:T){
return val.length;
}
getLen('hello');
const sum = <T extends number>(a: T, b: T): T => {
return (a + b) as T
}
let r = sum<number>(1, 2);
- 返回泛型中指定属性
const getVal = <T,K extends keyof T>(obj:T,key:K) : T[K]=>{
return obj[key];
}
8.兼容性
TS中的兼容性,主要看结构是否兼容。(核心是考虑安全性)
一.基本数据类型的兼容性
let temp:string | number;
let num!:number;
temp = num;
你要的我有就可以
let num:{
toString():string
}
let str:string = 'zf';
num = str; // 字符串中具备toString()方法,所以可以进行兼容
二.接口兼容性
interface IAnimal {
name: string,
age: number
}
interface IPerson {
name: string,
age: number,
address: string
}
let animal: IAnimal;
let person: IPerson = {
name: 'zf',
age: 11,
address: '回龙观'
};
animal = person;
接口的兼容性,只要满足接口中所需要的类型即可!
三.函数的兼容性
函数的兼容性主要是比较参数和返回值
-
参数
let sum1 = (a: string, b: string) => a + b; let sum2 = (a: string) => a; sum1 = sum2
赋值函数的参数要少于等于被赋值的函数,与对象相反,例如:
type Func<T> = (item: T, index: number) => void function forEach<T>(arr: T[], cb: Func<T>) { for (let i = 0; i < arr.length; i++) { cb(arr[i], i); } } forEach([1, 2, 3], (item) => { console.log(item); });
-
返回值
type sum1 = () => string | number type sum2 = () => string; let fn1: sum1; let fn2!: sum2; fn1 = fn2;
四.函数的逆变与协变
函数的参数是逆变的,返回值是协变的 (在非严格模式下函数的参数是双向协变的)
class Parent {
address: string = '回龙观'
}
class Child extends Parent {
money: number = 100
}
class Grandsom extends Child {
name: string = '吉姆';
}
type Callback = (person: Child) => Child
function execCallback(cb: Callback) { }
let fn = (person: Parent) => new Grandsom;
execCallback(fn);
通过这个案例可以说明,函数参数可以接收父类,返回值可以返回子类
五.类的兼容性
class Perent {
name: string = 'zf';
age: number = 11
}
class Parent1 {
name: string = 'zf';
age: number = 11
}
let parent: Perent = new Parent1
这里要注意的是,只要有private或者protected关键字类型就会不一致;但是继承的类可以兼容
class Parent1 {
protected name: string = 'zf';
age: number = 11
}
class Child extends Parent1{}
let child:Parent1 = new Child;
六.泛型的兼容性
interface IT<T>{}
let obj1:IT<string>;
let obj2!:IT<number>;
obj1 = obj2;
七.枚举的兼容性
enum USER1 {
role = 1
}
enum USER2 {
role = 1
}
let user1!:USER1
let user2!:USER2
user1 = user2 // 错误语法
不同的枚举类型不兼容
转载自:https://juejin.cn/post/7102384712504573982