前言
ts已经是前端必知必会的,未来只会更火,项目中也将应用的更多!
学起来吧!
本文前边部分比较基础,后半部分较深入,希望耐心看完,或多或少,一定会有收获!
一、ts简单介绍
TypeScript
是JavaScript
的超集,遵循ES6/ES5
规范,扩展了JavaScript
的语法。
TypeScript
最大特点是提示友好,对代码进行类型检查,编码阶段发现错误,避免线上问题。ts不能直接在浏览器运行,最终还是会被编译成js。
下面进入正题~
二、ts环境配置
1.全局安装TypeScript
,对ts
进行编译。
npm install typescript -g
tsc --init # 用来生成tsconfig.json配置文件
tsc # 可以将ts文件编译成js文件
tsc --watch # 监控ts文件变化生成js文件
用node环境执行ts:
① vscode插件:code runner
② 全局安装 ts-node -g
2.构建工具环境配置
ts的解析方式有两种:
- ts插件解析
- babel解析 rollup => rollup-plugin-typescript2
webpack => ts-loader babel-plugin-typescript
- 安装依赖
npm install rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
rollup-plugin-typescript2
是rollup
和typescript
沟通的桥梁。
@rollup/plugin-node-resolve
让rollup支持解析第三方模块。
rollup-plugin-serve
启动服务。
- 初始化ts配置文件
npx tsc --init
- 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', // umd, esm, cjs 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 dev
搞起!
三、基础类型
1.布尔、数字、字符串类型
let bool:boolean = true;
let num:number = 10;
let str:string = 'hello world';
2.元组类型
类型对应,个数固定
let tuple: [string, number, boolean] = ['hello', 10, true]; // 初始化时,必须按照类型定义进行赋值
tuple.push('world') // 只能增加定义过的类型
tuple[3] = 100; // 不能通过索引改变元组
3.数组
声明数组中元素数据类型
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];
4.枚举类型
- 普通枚举
enum LANG {
REACT,
VUE,
ANGULAR
}
// {0: "REACT", 1: "VUE", 2: "ANGULAR", REACT: 0, VUE: 1, ANGULAR: 2}
// 编译后的结果
(function (LANG) {
LANG[USER_ROLE["REACT"] = 0] = "REACT";
LANG[USER_ROLE["VUE"] = 1] = "VUE";
LANG[USER_ROLE["ANGULAR"] = 2] = "ANGULAR";
})(LANG || (LANG = {}));
- 异构枚举
enum LANG {
REACT = 'react',
VUE = 1,
ANGULAR
}
// {"REACT": "react", "VUE": 1, "ANGULAR": 2}
枚举支持反举,但仅限于索引,会自动进行推断。
- 常量枚举
enum LANG {
REACT,
VUE,
ANGULAR
}
console.log(LANG.VUE, LANG[0]) // 1 REACT
const enum LANG {
REACT,
VUE,
ANGULAR
}
console.log(LANG.REACT, LANG[0]) // 0 undefined
加上const后,不会生成一个对象,就不支持反举了。
5.any类型
6.null和undefined
null
和undefined
是任意类型的子类型。
如果strictNullChecks
值为true,或者严格模式下,则不能把null
和undefined
赋给其他类型,null
只能赋值给null
,undefined
只能赋值给undefined
。
let xx = number | boolean;
xx = null;
7.void类型
void的值只能赋予null和undefined,一般用于函数的返回值。
let a: void; // a = undefined;
let b: () => void; // 表示没有返回值,返回值是undefined
interface IC {
new (): void; // 表示不关心返回值类型
}
严格模式下,不能将null赋值给void。
void
用在接口中定义类中原型方法时,表示不关心返回值类型。在函数返回值中表示返回值是undefined
,即没有返回值。
8.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
}
}
9.object对象类型
object表示非原始类型。除number
,string
,boolean
,symbol
,null
,undefined
之外的类型。
let create = (obj:object):void=>{}
create({});
create([]);
create(function(){})
泛型约束会大量用object类型。
10.unknown 类型
1) unknown类型
表示类型未知,任何类型都可以赋值为unknown
类型。它是any
类型对应的安全类型。
let unknown:unknown;
unknown = 'tianshi';
unknown = 18;
不能访问
unknown
类型上的属性,不能作为函数、类来使用。
- 联合类型中的
unknown
type UnionUnknown = unknown | null | string | number; // type UnionUnknown = unknown
其他类型与
unknown
联合,结果都是unknown
类型。
- 交叉类型中的
unknown
type inter = unknown & null; // type inter = null
其他类型与
unknown
交叉,结果都是其他类型。
2) unknown特性
never
是unknown
的子类型
type isNever = never extends unknown ? true : false; // type isNever = true
keyof unknown
是never
type key = keyof unknown; // type key = never
unknown
类型不能被遍历
type IMap<T> = {
[P in keyof T]: number
}
type t = IMap<unknown>; // type t = {}
unknown
类型不能和number
类型进行+
运算
let p1: unknown;
let p2: number;
let p = p1 + p2 // 类型报错
11.Symbol类型
Symbol表示独一无二
const s1 = Symbol('key');
const s2 = Symbol('key');
console.log(s1 == s2); // false
12.BigInt类型
BigInt是大于2^53-1的值
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)); // false
number类型和bigInt类型是不兼容的。
四、类型断言
1.联合类型
// 普通联合类型
let numOrStr: string | number;
let el: HTMLElement | null = document.getElementById('id');
// 字面量类型
// 类型的内容是固定的。如果类型很复杂,我们希望后续复用,可以把类型单独提取出来。
type IType = 'a' | 'b' | 'c';
let type1: IType = 'b';
let type2: IType = 'c';
2.非空断言!
let el: HTMLElement | null = document.getElementById('id');
el!.innerHTML = 'xxx';
3.强制转化类型 as | <>
强转类型,必须保证联合类型中有才可以。
let xx = string | number | undefined;
(xx as string).split('');
(<string>xx).split(''); // 和jsx冲突,不建议使用
4.双重断言
(xx as any) as boolean;
五、函数类型
1.函数的两种声明方式
// 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;
};
函数重载只能用函数声明的写法。使用类型别名只能用函数表达式的写法。
2.可选参数
let sum = (a: string, b?: string):string => {
return a + b;
};
sum('a'); // 可选参数必须在其他参数的最后面
3.默认参数
let sum = (a: string, b: string = 'b'): string => {
return a + b;
};
sum('a'); // 默认参数必须在其他参数的最后面
4.剩余参数
const sum = (...args: string[]): string => {
return args.reduce((memo, current) => memo += current, '')
}
sum('a', 'b', 'c', 'd');
5.函数重载
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');
六、类
1.ts中定义类
class Pointer{
x: number = 1; // 声明的变量会被增加到实例上,实例上的属性必须先声明
y!: number;
constructor(x:number, y?:number, ...args:number[]){
this.x = x;
this.y = y as number;
}
}
let p = new Pointer(100,200);
实例上属性必须先声明再使用。构造函数中的参数可以使用可选和剩余参数。
2.类中的修饰符
pubic
修饰符 自己、子类、实例均可以访问到。
class Animal {
constructor(public name: string, public age: number) {
}
}
class Cat extends Animal {
constructor(name: string, age: number) {
super(name, age);
console.log(this.name,this.age); // 子类访问 Tom 18
}
}
let p = new Cat('Tom', 18);
console.log(p.name,p.age); // 外层访问 Tom 18
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');
3.静态属性和方法、属性访问器
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); // Tom
console.log(Animal.getName()); // 动物类
console.log(Animal.type); // '哺乳动物'
静态属性和方法是可以被子类继承的
4.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()) // 动物
console.log(cat.say()); // 猫猫叫
5.类的装饰器
装饰器,就是一个函数,不要把他想的很复杂。
1)装饰类
function addSay(target:any){
target.prototype.say = function(){console.log('say')}
}
@addSay
class Person {
say!:Function
}
let person = new Person
person.say(); // say
装饰类可以给类扩展功能,需要开启
experimentalDecorators:true
。
2)装饰类中的属性
// target => Person.prototype
function toUpperCase(target:any,key:string){
console.log(target, key);
let value = target[key];
Object.defineProperty(target,key,{
get(){
return value.toUpperCase();
},
set(newValue){
value = newValue
}
})
}
// target => Person
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 = 'XiaoTianShi'
@double
static age: number = 10;
getName() {
return this.name;
}
}
let person = new Person();
console.log(person.getName(),Person.age) // XIAOTIANSHI 20
装饰属性可以对属性的内容进行改写。装饰的是实例属性则target指向类的原型,装饰的是静态属性,则target指向类本身。
3)装饰类中的方法
function get(params: any) {
return function (target:any,key:string,descriptor:PropertyDescriptor) {
console.log(descriptor);
// {
// value: [Function],
// writable: true,
// enumerable: true,
// configurable: true
// }
// 修改类中定义的方法,把装饰器方法里面传入的所有参数改为string类型
let oMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
args = args.map(val => {
return String(val)
});
oMethod.apply(this, args);
}
}
}
class Person {
name: string = 'TianShi'
static age: number = 10;
@get('方法入参')
getName(...args: any[]) {
console.log(args, this.name); // ['111', '111'] // TianShi
}
}
let person = new Person();
console.log(person.getName(111, '111'));
装饰方法,可以对方法进行重写。装饰的是实例方法则target指向类的原型,装饰的是静态方法,则target指向类本身。
4)装饰方法中的参数
function addPrefix(params: any){
return function(target:any,key:string,paramIndex:number){
console.log(target,key,paramIndex, params, params[paramIndex]); // Person.prototype getName 0 uuid u
// 扩展属性
target.expendProp = params;
}
}
class Person {
name: string = 'XiaoTianShi'
prefix!:string
getName(@addPrefix('uuid') prefix:string) {
console.log(prefix); // -ts
return this.name;
}
}
let person: any = new Person();
console.log(person.getName('-ts')); // XiaoTianShi
console.log(person.expendProp); // uuid
参数装饰器表达式会在运行时当做函数被调用,可以使用参数装饰器为类的原型增加一些原始数据。
接收3个参数:
1.对于静态成员来说是构造函数,对于实例成员是类的原型对象。
2.方法的名字。
3.参数咋函数参数列表中的索引。
5)装饰器执行顺序
属性装饰器 => 方法装饰器 => 参数装饰器2 => 参数装饰器1 => 类装饰器2 => 类装饰器1
function logClass1(params:string) {
return function(target:any) {
console.log('类装饰器1');
}
}
function logClass2(params:string) {
return function(target:any) {
console.log('类装饰器2');
}
}
function logAttribute(params?:string) {
return function(target:any, attr:any) {
console.log('属性装饰器');
}
}
function logMethod(params?:string) {
return function(target:any, methodName:any, desc: any) {
console.log('方法装饰器');
}
}
function logParams1(params?:string) {
return function(target:any, methodName:any, paramsIndex: any) {
console.log('参数装饰器1');
}
}
function logParams2(params?:string) {
return function(target:any, methodName:any, paramsIndex: any) {
console.log('参数装饰器2');
}
}
@logClass1('hello')
@logClass2('world')
class HttpClient {
@logAttribute()
public apiUrl: string | undefined;
constructor() {}
@logMethod()
getData() {}
setData(@logParams1() param1:any, @logParams2() param2:any) {
}
}
let p:any = new HttpClient();
6.抽象类
抽象类无法被实例化,只能被继承。抽象方法必能在抽象类中实现,只能在抽象类的具体子类中实现,而且,必须实现。
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
表示函数的返回值为空(不关心返回值类型)。
七、接口
接口,interface
描述对象的形状和结构,方便复用。
1.函数接口参数
interface IPerson {
name:string;
age:number;
}
type IPerson = {name: string, age: number} | string;
const people = (obj:IPerson):string =>{
return `${obj.name}今年${obj.age}岁!`
}
people('xiaotianshi', 18)
通过接口描述和约束了函数的参数。
interface
可以被类实现和继承,type
不可以。type
可以用联合类型,interface
写联合类型比较恶心。
2.函数类型接口
interface ISum {
(a: string, b: string): string;
}
const sum: ISum = (a, b) => {
return a + b;
}
sum('1', '2');
通过接口限制函数的参数类型和返回值类型。
3.函数混合类型
给函数定义一个属性,我们应该怎么描述它的类型呢?使用一个计数器的例子来看下,每次调用就+1。
interface ICounter {
(): number; // 限制函数类型
count: 0 // 限制函数上的属性
}
let fn: ICounter = (() => {
return fn.count++;
}) as ICounter;
fn.count = 0;
console.log(fn()); // 0
console.log(fn()); // 1
4.对象接口
// 只读属性
interface IVegetables {
readonly color: string,
size: string
}
// 可选属性,联合类型
interface IVegetables {
price?: number,
taste: 'sour' | 'sweet'
}
// 任意属性
interface IVegetables {
[key: string]: any;
}
const tomato: IVegetables = {
color: 'red',
size: '10',
taste: 'sour',
shape: 'circle'
}
// 多余的属性除了任意属性外,也可以使用类型断言
const tomato: IVegetables = {
color: 'red',
size: '10',
taste: 'sour',
type: '蔬菜'
} as IVegetables;
// 仅读属性不能进行修改
tomato.color = 'green';
?
表示可选属性,readonly
标识的是只读属性,不可以修改。 相同命名的接口将会合并,会改变原有接口。
5.任意属性,可索引接口
interface Person {
name: string;
[key: string]: any
}
let p: Person = {
name: 'xiaotianshi',
age: 18,
[Symbol()]: 'high'
}
任意属性可以对一部分属性做限制,其余的属性随意增减。
interface IArr {
[key: number]: any
}
let p: IArr = {
0: '1', 1: '2', 3: '3'
}
let arr: IArr = [1, 'd', 'c'];
可索引接口用于标识数组。
6.类接口
interface Speakable {
name: string;
speak(): void;
}
interface ChineseSpeakable {
speakChinese(): void
}
class Speak implements Speakable, ChineseSpeakable {
name!: string
speak() {}
speakChinese() {}
}
一个类可以实现多个接口,类中必须要实现接口中的属性和方法
7.接口继承
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.");
}
}
8.构造函数类型
类可以充当类型,描述实例。
// type IPerson = new (name: string) => Person;
interface IPerson<T> {
new (name: string): T
}
function createInstance<T>(clazz: IPerson<T>, name: string) {
return new clazz(name);
}
class Person {
age: 18;
constructor(public name: string){}
}
class Dog {
color: 'yellow';
constructor(public name: string) {}
}
let r = createInstance<Dog>(Dog, '小狗'); // r的类型是Dog,可以.出来color和name
type
和interface
的不同写法。new() 表示当前是一个构造函数类型。 为啥用到了泛型呢?因为返回值类型不能确定,我们不知道是Dog还是Person。 类的类型是用来描述实例的。
八、泛型
通常是执行时才能确定类型的时候,需要用到泛型。通常在函数、类、接口、别名中使用。
1.指定函数参数类型
- 单个泛型
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']))
2.函数标注的方式
- 类型别名
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]]
}
type
多与keyof
作为联合类型使用,interface
可以被继承和实现,扩展性更好。
3.泛型接口使用
interface ICreateArray<T> { // 这里的T是使用接口的时候传入
<U>(a: T, b: U): U[] // 这里的U是调用函数的时候传入
}
let createArray: ICreateArray<number> = <U>(times: number, value: U): U[] => {
let result = [];
for (let i = 0; i < times; i++) {
result.push(value);
}
return result;
}
createArray(1, 'xxx');
泛型传入地方不同,调用时机也不同。放在函数前,表示调用函数时确定类型,放在接口后面,表示使用接口的时候确定类型。
4.默认泛型
interface T2<T = string> {
name: T
}
type T22 = T2;
let p: T22 = { name: 'tianshi' } // p的类型是T2
默认泛型和函数默认参数类似,用于指定泛型的默认类型。
5.类中的泛型
- 创建实例时,提供类型
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());
- 构造函数类型
interface IPerson<T> {
new (name: string, age: number): T
}
const createClass = <T>(clazz: IPerson<T>, name: string, age: number) => {
return new clazz(name, age);
}
class Person {
constructor(public name: string, public age: number) { }
}
let r = createClass<Person>(Person, 'xxx', 18);
6.泛型约束
- 属性约束
interface IWithLength {
length:number
}
const computeArrayLength = <T extends IWithLength, K extends IWithLength>(arr1: T, arr2: K): number => {
return arr1.length + arr2.length;
}
computeArrayLength('123', {length: 3});
computeArrayLength([1, 2], '34');
必须都有length属性才行。
const sum = <T extends number>(a: T, b: T): T => {
return (a + b) as T
}
let r = sum<number>(1, 2);
类型必须是number才行。
- 对象约束
const getVal = <T extends object, K extends keyof T>(obj: T, key: K): T[K] => {
return obj[key];
}
getVal({a: 1, b: 2}, 'b');
第二个参数,必须是第一个参数中的某个键。
九、兼容性
这里的兼容性和我们浏览器的兼容性不同,ts
的兼容性主要是看结构是否兼容,核心是考虑安全性
。
1.基本数据类型的兼容性
let temp: string | number;
let num!: number;
temp = num;
temp
中有num
的类型,所以可以兼容,可以赋值。
let num: {
toString(): string
}
let str: string = 'tianshi';
num = str;
字符串中具备toString()方法,所以可以进行兼容,可以赋值。
2.接口的兼容性
interface IAnimal {
name: string,
age: number
}
interface IPerson {
name: string,
age: number,
address: string
}
let animal: IAnimal;
let person: IPerson = {
name: 'will',
age: 18,
address: '海淀'
};
animal = person;
接口的兼容性,只要满足接口中所需要的类型即可。人是动物,但是动物不一定是人。
3.函数的兼容性
函数的兼容性,多是比较参数和返回值。
- 参数
let sum1 = (a: string, b: string) => a + b;
let sum2 = (a: string) => a;
sum1 = sum2
可以传递2个,但是你传递了1个,是安全的。
只能传递1个参数,但是你给了2个,不安全。
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); // item和index都返回了
}
}
forEach([1, 2, 3], (item) => { // 用到了item,没有用index,不传index也可以
console.log(item);
});
赋值函数的参数要小于等于被赋值的函数,与对象相反。
- 返回值
type sum1 = () => string | number
type sum2 = () => string;
let fn1!: sum1;
let fn2!: sum2;
fn1 = fn2;
type sum1 = () => { name: string };
type sum2 = () => { name: string; age: number };
let fn1!: sum1;
let fn2!: sum2;
fn1 = fn2;
4.函数的逆变协变
函数的参数是逆变的,可以传递父类。函数的返回值是协变的,可以返回子类。(在非严格模式下,函数的参数是双向协变的)。
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);
参数,接受Child
,传入Parent
。返回值,本来返回Child
,可以返回Grandsom
。缩小入参返回,扩大返回值范围。目的是为了安全性。
以上,是对类型的兼容性的阐述,告诉你类型可以这么用。从应用层面说,实际应用的时候,入参需要
Child
,就传Child
,传Parent
会报错。
5.类的兼容性
class Perent {
name: string = 'tianshi';
age: number = 18
}
class Parent1 {
name: string = 'tianshi';
age: number = 18
}
let parent: Perent = new Parent1;
注意:只要有private或者protected修饰符,类型就会不一致。但是继承的类可以兼容。
class Parent {
protected name: string = 'tianshi';
age: number = 18
}
class Child extends Parent { };
let child: Parent = new Child;
6.泛型的兼容性
interface MyType<T> { }
let obj1: MyType<string>;
let obj2: MyType<number>;
obj1 = obj2;
7.枚举的兼容性
enum USER1 {
role = 1
}
enum USER2 {
role = 1
}
let user1!: USER1
let user2!: USER2
user1 = user2 // 错误语法
不同的枚举类型不兼容。
十、类型保护
所谓类型保护,就是判断执行的代码片段,自动识别出变量属性和方法。
1.typeof
类型保护
function double(val: number | string) {
if (typeof val === 'number') {
val. // 可以点出来数字类型的属性和方法
} else {
val. // 可以点出来字符串类型的属性和方法
}
}
2.instanceof
类型保护
class Cat { eat(){} }
class Dog { drink(){} }
const getInstance = (clazz: { new(): Cat | Dog }) => {
return new clazz();
}
let r = getInstance(Cat);
if (r instanceof Cat) {
r // 猫
} else {
r // 狗
}
3.in
类型保护
interface Fish {
swiming: string,
}
interface Bird {
fly: string,
leg: number
}
function getType(animal: Fish | Bird) {
if ('swiming' in animal) {
animal // Fish
} else {
animal // Bird
}
}
4.可识别联合类型
通过增加一个唯一的自定义属性,来识别类型。
增加一个class
属性,通过该属性判断是用的WarningButton
类型,还是DangerButton
类型。
interface WarningButton {
class: 'warning'
}
interface DangerButton {
class: 'danger'
}
function createButton(button: WarningButton | DangerButton) {
if (button.class == 'warning') {
button // WarningButton
} else {
button // DangerButton
}
}
5.null保护?? ||
const addPrefix = (num?: number) => {
num = num || 1.1; // 确保外层取值是有真值的
let out = num.toFixed(); // 外层作用域不需要使用可选链,因为 || 已经确保有值。
function prefix(fix: string) {
return fix + num?.toFixed() // ?.内层作用域还是需要使用可选链判断
}
return prefix('zxd');
}
console.log(addPrefix());
注意:
ts
无法检测内部函数变量类型。
6.自定义类型保护is
interface Fish {
swiming: string,
}
interface Bird {
fly: string,
leg: number
}
function isFish(animal: Fish | Bird): animal is Fish {
return 'swiming' in animal
}
function getAniaml(animal: Fish | Bird) {
if (isFish(animal)) {
animal // 可以点出来鱼的属性
} else {
animal // 可以点出来鸟的属性
}
}
getAniaml({ swiming: 'swim' });
function isString(val: any): val is string {
return Object.prototype.toString.call(val) === '[object String]';
}
let str = '123';
if (isString(str)) {
str
}
is
关键字,它被称为类型谓词,用来判断一个变量是否属于某个接口或类型。如果需要封装一个类型判断函数,应该第一时间想到它。animal is Fish, val is string
返回是一个boolean
值,即表示返回值是一个布尔值。
7.完整性保护
完整性保护,保证代码逻辑全部都覆盖到。主要靠never
,利用never
无法到达最终结果的特性,来保证代码的完整性。
interface ICircle {
kind: 'circle',
r: number
}
interface IRant {
kind: 'rant',
width: number,
height: number
}
interface ISquare {
kind: 'square',
width: number
}
type Area = ICircle | IRant | ISquare
const isAssertion = (obj: never) => { }
const getArea = (obj: Area) => {
switch (obj.kind) {
case 'circle':
return 3.14 * obj.r ** 2
+ case 'rant':
+ return obj.width * obj.height
+ case 'square':
+ return obj.width * obj.width
default:
return isAssertion(obj); // 必须实现所有逻辑
}
}
getArea({kind: 'square', width: 100});
必须实现所有逻辑才可以。
十一、类型推断
ts有非常棒的类型推断能力,不需要我们定义用到的所有类型。
1.赋值推断
let str = 'tianshi';
let age = 18;
let boolean = true;
赋值时推断,会根据赋值推断出变量类型。
2.返回值推断
自动推断函数返回值类型。
function sum(a: string, b: string) {
return a + b;
}
let r = sum('a', 'b');
3.函数推断
从左到右,依次推断参数类型,返回值类型。
type Sum = (a: string, b: string) => string;
const sum: Sum = (a, b) => a + b;
4.属性推断
通过属性值,推断出属性的类型。
let person = {
name: 'tianshi',
age: 18
}
let { name, age } = person;
5.类型反推
使用typeof
关键字反推变量类型。
let person = {
name: 'tianshi',
age: 18
}
type Person = typeof person; // {name: string, age: number}
6.索引访问操作符
通过[]
访问
interface IPerson {
name: string,
age: number,
job: {
address: string
}
}
type IJob = IPerson['job']['address']; // type IJob = string;
注意:只能通过
[]
访问接口的类型,.
无效。
7.类型映射
interface IPerson {
name: string,
age: number
}
type IMapPerson = { [key in keyof IPerson]: IPerson[key] } // type IMapPerson = { name: string, age: number };
keyof
关键字,用来获取到接口定义的键,同js中对象的Object.keys()类似。[key in keyof IPerson]: IPerson[key]
标识遍历IPerson的key,并赋值类型。
十二、交叉类型
交叉类型(Intersection Type)是将多个类型合并为一个类型。在原有的类型基础上想去扩展属性,可以用交叉类型。
通常,我们用&
符用来实现交叉类型。
interface Person1 {
handsome: string,
}
interface Person2 {
high: string,
}
type P1P2 = Person1 & Person2;
let p: P1P2 = { handsome: '帅', high: '高' };
交叉类型类似于数学中的交集,比如两拨人,一拨很高,一拨很帅,我们要找出交叉部分,又高又帅的人。
function mixin<T, K>(a: T, b: K): T & K {
return { ...a, ...b }
}
const newType = mixin({ name: 'tianshi' }, { age: 18 })
// newType: { name: string } & { age: number }
interface IPerson1 {
name: string,
age: number
}
interface IPerson2 {
name: number
age: number
}
type person = IPerson1 & IPerson2
let name!: never
let person: person = { name, age: 11 };
两个属性之间 string & number 的值为 never。因为不可能存在是字符串同时又是数字类型的属性。
ts
核心是为了安全,交叉类型可以赋予给没有交叉前的类型。
十三、条件类型
1.条件类型基本使用
使用extends
和三元表达式,实现条件判断
interface Fish {
name1: string
}
interface Water {
name2: string
}
interface Bird {
name3: string
}
interface Sky {
name4: string
}
type Condition<T> = T extends Fish ? Water : Sky;
let con1: Condition<Fish> = { name2: '水' };
2.条件类型分发
let c: Condition<Fish | Bird> = { name2: '水' };
// 类型会依次进行分发,最终采用联合类型作为结果。等价于:
type c1 = Condition<Fish>;
type c2 = Condition<Bird>;
type c = c1 | c2
一个例子:当传入name
属性时,age
属性必传。当不传name
属性时,age
属性可选。
interface ISchool1 {
name: string;
age: number
}
interface ISchool2 {
age?: number;
size: string;
}
type School<T> = T extends { name: string } ? ISchool1 : ISchool2;
let school: School<{name: 'xxx'}> // let school: ISchool1
3.内置条件类型
Exclude
排除类型
type Exclude<T, U> = T extends U ? never : T;
type MyExclude = Exclude<'1' | '2' | '3', '1' | '2'> // MyExclude = '3'
Extract
抽取类型
type Extract<T, U> = T extends U ? T : never;
type MyExtract = Extract<'1' | '2' | '3', '1' | '2'> // MyExtract = "1" | "2"
NonNullable
非空检测
type NonNullable<T> = T extends null | undefined ? never : T
type MyNone = NonNullable<'a' | null | undefined> // MyNone = 'a'
4.infer类型推断
infer
表示,在extends
条件语句中,待推断的类型变量。(只能用在extends
条件语句中)
ReturnType
返回值类型
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;
在这个条件语句中,infer R
表示待推断的函数返回值。
整句表示:如果T
能赋值给(...args: any) => infer R
,则结果是(...args: any) => infer R
类型中的返回值R
,否则返回never
。
type ReturnType<T> = T extends (...args: any) => infer R ? R : never
function getUser(name: string, age: number) {
return { name: 'tianshi', age: 18 }
}
type MyReturn = ReturnType<typeof getUser> // MyReturn = { name: string, age: number };
type MyString = ReturnType<string> // MyString = never
Parameters
参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : any;
在这个条件语句中,infer R
表示待推断的参数类型。
整句表示:如果T
能赋值给(...args: infer P) => any
,则结果是(...args: infer P) => any
类型中的参数P
,否则返回any
。
function getUser(name: string, age: number) {
return { name: 'tianshi', age: 18 };
}
type MyParams = Parameters<typeof getUser>; // MyParams = [string, number]
type MyString = Parameters<string>; // MyString = any
ConstructorParameters
构造函数参数类型
type ConstructorParameters<T> = T extends { new (...args: infer P): any } ? P : never;
在这个条件语句中,infer P
表示待推断的构造函数参数类型。
整句表示:如果T
能赋值给new (...args: infer P): any
,则结果是new (...args: infer P): any
类型中构造函数参数P
,否则返回never
。
class Person {
constructor(name: string, age: number) { }
}
type MyConstructor = ConstructorParameters<typeof Person> // MyConstructor = [string, number]
type MyString = ConstructorParameters<string> // MyString = never
InstanceType
实例类型
type InstanceType<T> = T extends { new(...args: any): infer R } ? R : any;
在这个条件语句中,infer R
表示待推断的实例类型。
整句表示:如果T
能赋值给new(...args: any): infer R
,则结果是new(...args: any): infer R
类型中构造函数参数R
,否则返回any
。
class Person {
constructor(name: string, age: number) { }
}
type MyInstance = InstanceType<typeof Person>; // MyInstance = Person
type MyString = InstanceType<string>; // MyString = any
5.infer实践
- 将数组类型转化为联合类型
type ElementOf<T> = T extends Array<infer E> ? E : never;
在这个条件语句中,infer E
表示待推断的数组元素类型。
整句表示:如果T
能赋值给Array<infer E>
,则结果返回Array<infer E>
数组每项的类型,否则返回never
。
type TupleToUnion = ElementOf<[string, number, boolean]>; // TupleToUnion = string | number | boolean;
type TupleString = ElementOf<string>; // TupleString = never;
- 将两个函数的参数转化为交叉类型
type ToIntersection<T> = T extends ([(x: infer U) => any, (x: infer U) => any]) ? U : never;
在这个条件语句中,infer U
表示待推断的函数参数类型。
整句表示:要把T1
、T2
赋予给x
,那么x
的值就是T1
、T2
的交集。
type T1 = { name: string };
type T2 = { age: number };
type t3 = ToIntersection<[(x: T1) => any, (x: T2) => any]> // t3 = T1 & T2
type t4 = ToIntersection<[string, string]> // t4 = never
十四、内置类型
1.partial
转化可选属性
type Partial<T> = { [K in keyof T]?: T[K] }
原理就是遍历所有属性,并设置为可选属性。
interface Company {
num: number
}
interface Person {
name: string,
age: string,
company: Company
}
type PartialPerson = Partial<Person>; // { name: string, age: string, company: Company }
注意:无法实现深度转化。
实现一个深度转化,如果值是对象,则继续深度转化
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
type DeepPartialPerson = DeepPartial<Person>;
2.Required
转化必填属性
type Required<T> = {[K in keyof T]-?:T[K]}
原理同Paritial
相同,Partial
是将必填转化为选填,Required
使用-?
将选填转为必填。
interface Company {
num: number
}
interface Person {
name: string,
age: string,
company: Company
}
type PartialPerson = Partial<Person>;
type RequiredPerson = Required<PartialPerson>
3.Readonly
转化仅读属性
type Readonly<T> = { readonly [K in keyof T]: T[K] }
实现原理是给每个属性增加readonly
修饰符,转为可读属性。
interface Company {
num: number
}
interface Person {
name: string,
age: string,
company: Company
}
type RequiredPerson = Readonly<Person>
4.Pick
在已有类型中挑选所需属性
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
分析一下,T
表示传入的对象类型,K extends keyof T
表示约束K
的类型,必须是T
的属性。[P in K]: T[P]
表示从T
中挑选出属于K
的属性及类型并返回。
interface Company {
num: number
}
interface Person {
name: string,
age: string,
company: Company
}
type PickPerson = Pick<Person, 'name' | 'age'> // pickPerson = { name: string, age: string }
5.Record
记录类型
type Record<K extends keyof any, T> = { [P in K]: T }
分析一下,K extends keyof any
泛型约束,表示K
的类型是string, number, symbol
中的任意一种。(可以作为对象的键的类型)
[P in K]: T
表示键是string, number, symbol
中的任意一种,值是传入的泛型T
。
let person: Record<string, any> = { name: 'tianshi', age: 18 };
我们经常用record
类型表示映射类型。
function map<T extends keyof any, K, U>(obj: Record<T, K>, callback: (item: K, key: T) => U): Record<T, U> {
let result = {} as Record<T, U>
for (let key in obj) {
result[key] = callback(obj[key], key)
}
return result
}
const r = map({ name: 'xiaotianshi', age: 18 }, (item, key) => {
return item
});
// r = Record<"name" | "age", string | number>
逐句分析:
<T extends keyof any, K, U>
三个类型形参,T被约束为string, number, symbol
类型中的一种。obj: Record<T, K>
定义obj对象的键是T
类型,值是K
类型。callback: (key: T, value: K) => U
定义callback的类型,入参key的类型是T
,value的类型是K
,返回值类型是U
。Record<T, U>
定义返回的对象类型,键是T
类型,值是U
类型。
本例中,通过入参{ name: 'xiaotianshi', age: 18 }
可以推断出T
是类型 string
,K
是string | number
。通过let result = {} as Record<T, U>
可以推断出U
的类型是callback
函数的返回值的类型,其实和K
类型一样,也是string | number
。
6.Omit
忽略属性
type Exclude<T, U> = T extends U ? never : T;
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
分析一下:
Exclude<keyof T, K>
表示从keyof T
中排除掉K
。
Pick<T, Exclude<keyof T, K>>
然后从T
中挑选出刚才排除掉K
剩下的属性。
let person = {
name: 'tianshi',
age: 18,
address: '海淀'
}
type OmitAddress = Omit<typeof person, 'address'> // type OmitAddress = { name: string; age: number; }
忽略
person
中的address
属性,先排除到address
属性,然后在person
中挑选出需要的属性。
十五、装包和拆包
1.装包
type Proxy<T> = {
get(): T,
set(value: T): void
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>
}
let props = {
name: 'tianshi',
age: 18
}
function proxify<T>(obj: T): Proxify<T> {
let result = {} as Proxify<T>;
for (let key in obj) {
let value = obj[key];
result[key] = {
get() {
return value
},
set: (newValue) => value = newValue
}
}
return result
}
let proxyProps = proxify(props);
// let proxyProps: Proxify<{ name: string; age: number; }>
2.拆包
function unProxify<T>(proxyProps: Proxify<T>): T {
let result = {} as T;
for (let key in proxyProps) {
let value = proxyProps[key];
result[key] = value.get()
}
return result
}
let proxy = unProxify(proxyProps);
// let proxy: { name: string; age: number; }
十六、自定义类型
1.Diff
实现
let person1 = {
name: 'tianshi',
age: 18,
address: '中国'
}
let person2 = {
address: '中国',
}
type Diff<T extends object, K extends object> = Omit<T, keyof K>
type DiffPerson = Diff<typeof person1, typeof person2>
// type DiffPerson = { name: string; age: number; }
2.InterSection
交集
let person1 = {
name: 'tianshi',
age: 18,
address: '中国'
}
let person2 = {
address: '中国',
}
type InterSection<T extends object, K extends object> = Pick<T, Extract<keyof T, keyof K>>
type InterSectionPerson = InterSection<typeof person1, typeof person2>
// type InterSectionPerson = { address: string; }
3.Overwrite
属性覆盖
如果存在已有属性则使用新属性类型进行覆盖操作
type OldProps = { name: string, age: number, visible: boolean };
type NewProps = { age: string, other: string };
type Diff<T extends object, K extends Object> = Omit<T, keyof K>
type InterSection<T extends object, K extends object> = Pick<T, Extract<keyof T, keyof K>>
type Overwrite<T extends object, K extends object, I = Diff<T, K> & InterSection<K, T>> = Pick<I, keyof I>
type ReplaceProps = Overwrite<OldProps, NewProps>
// type ReplaceProps = { name: string; age: string; visible: boolean; }
4.Merge
对象合并
将两个对象类型进行合并操作。
type OldProps = { name: string, age: number, visible: boolean };
type NewProps = { age: string, other: string };
type Compute<A extends any> = { [K in keyof A]: A[K] };
type Merge<T, K> = Compute<Omit<T, keyof K> & K>;
type MergeObj = Merge<OldProps,NewProps>
// type MergeObj = { name: string; visible: boolean; age: string; other: string; }
十七、模块和命名空间
默认情况下,我们编写的代码处于全局命名空间下。
1.模块
文件模块:在你的TypeScript
文件的根级别位置还有import
或者export
,那么会在这个文件中创建一个本地的作用域。
// a.ts导出
export default 'a';
// index.ts导入
import name from './index';
2.命名空间namespace
命名空间可以用于组织代码,避免文件内命名冲突。
- 命名空间的使用
export namespace zoo {
export class Dog { eat() { console.log('zoo dog'); } }
}
export namespace home {
export class Dog { eat() { console.log('home dog'); } }
}
let dog_of_zoo = new zoo.Dog();
dog_of_zoo.eat();
let dog_of_home = new home.Dog();
dog_of_home.eat();
- 命名空间嵌套使用
export namespace zoo {
export class Dog { eat() { console.log('zoo dog'); } }
export namespace bear{
export const name = '狗熊'
}
}
console.log(zoo.bear.name); // 狗熊
命名空间中导出的变量,可以通过命名空间使用。
十八、类型声明
1.声明全局变量
1) 普通类型声明
declare let age: number;
declare function sum(a: string, b: string): void;
declare class Animal { };
declare const enum Seaons {
Spring,
Summer,
Autumn,
Winter
}
declare interface Person {
name: string,
age: number
}
类型声明在编译时会被删除,不会影响真正代码执行。
- 一个例子,声明jQuery类型 jQuery通过外部CDN方式引入,想在代码中直接使用。
declare const $: (selector: string) => {
height(num?: number): void
width(num?: number): void
};
$('').height();
2) 命名空间声明
declare namespace jQuery {
function ajax(url: string, options: object): void;
namespace fn {
function extend(obj: object): void
}
}
jQuery.ajax('/', {});
jQuery.fn.extend({});
namespace
表示一个全局变量包含很多子属性,命名空间内部不需要使用declare
声明属性或方法。
2.类型声明文件
类型声明文件以.d.ts
结尾,默认在项目编译时会查找所有以.d.ts
结尾的文件。
// jquery.d.ts
declare const $: (selector: string) => {
height(num?: number): void
width(num?: number): void
};
declare namespace jQuery {
function ajax(url: string, otpions: object): void;
namespace fn {
function extend(obj: object): void
}
}
3.编写第三方声明文件
配置tsconfig.json
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
- jquery声明文件
// types/jquery/index.d.ts
declare function jQuery(selector: string): HTMLElement;
declare namespace jQuery {
function ajax(url: string): void
}
export = jQuery;
- events模块声明文件 使用:
import { EventEmitter } from "events";
var e = new EventEmitter();
e.on('message', function (text) {
console.log(text)
})
e.emit('message', 'hello');
声明文件:
export type Listener = (...args: any[]) => void;
export type Type = string | symbol
export class EventEmitter {
static defaultMaxListeners: number;
emit(type: Type, ...args: any[]): boolean;
addListener(type: Type, listener: Listener): this;
on(type: Type, listener: Listener): this;
once(type: Type, listener: Listener): this;
}
4.模块导入导出
import $ from 'jquery' // 只适用于 export default $
const $ = require('jquery'); // 没有声明文件可以直接使用 require语法
import * as $ from 'jquery' // 为了支持 Commonjs规范 和 AMD规范 导出时采用export = jquery
import $ = require('jquery') // export = jquery 在commonjs规范中使用
5.第三方声明文件
@types
是一个约定的前缀,所有第三方声明的类型库都会带有这样的前缀。
npm install @types/jquery -S
查找规范:
- node_modules/jquery/package.json 中的types字段
- node_modules/jquery/index.d.ts
- node_modules/@types/jquery/index.d.ts
十九、扩展全局变量类型
1.扩展局部变量
可以使用接口对已有类型进行扩展。
interface String {
double(): string
}
// String.prototype.double = function () {
// return this as string + this;
// }
let str = 'tianshi'; // 可以点出来double
interface Window {
mynane: string
}
console.log(window.mynane)
2.模块内全局扩展
declare global {
interface String {
double(): string;
}
interface Window {
myname: string
}
}
3.声明合并
同一名称的两个独立声明最终会被合并成一个单一声明,合并后的声明拥有原先两个声明的特性。
1)同名接口合并
interface Animal {
name: string
}
interface Animal {
age: number
}
let a: Animal = { name: 'tianshi', age: 18 };
2)命名空间的合并
- 扩展类 给类添加静态成员
class Form { }
namespace Form {
export const type = 'form'
}
console.log(Form.type); // form
- 扩展方法
function getName() { }
namespace getName {
export const type = 'form'
}
console.log(getName.type); // form
- 扩展枚举类型
enum Seasons {
Spring = 'Spring',
Summer = 'Summer'
}
namespace Seasons {
export let Autum = 'Autum';
export let Winter = 'Winter'
}
console.log(Seasons.Autum) // Autum
3)交叉类型合并
import { Store } from 'redux';
type StoreWithExt = Store & {
ext: string
}
let store: StoreWithExt;
4.生成声明文件
配置tsconfig.json
为true
生成声明文件。
"declaration": true