文章目录
前言
本文记录TypeScript一些高级用法,首先要有一定的TypeScript基本知识和了解基础的ES6语法,如果还没学习过TypeScript,推荐2个网站学习后再来看这篇文章。
- TypeScript官方文档
- 蚂蚁部落的TypeScript教程 (建议先看这个)
蚂蚁部落的TypeScript教程要精练一些,可以作为快速上手的教程。官方文档比较详细,可能一开始看会有些难懂。
ts数据类型
- 布尔类型(boolean)
- 数字类型(number)
- 字符串类型(string)
- 数组类型(array)
- 元祖类型(tuple)
- 枚举类型(enum)
- 任意类型(any)
- null 和 undefined
- void类型
- never类型
null 和 undefined类型
默认情况下null和undefined是所有类型的子类型,也就是可以赋值给其他类型。
undefined出现原因:
- 变量被声明了但是没赋值时
- 调用函数时,应该提供的参数没提供,则该参数为undefined
- 函数没有返回值时,默认返回undefined
- 对象没有赋值的属性
null出现原因:
- 正则捕获的时候,如果没有捕获到结果,默认也是Null
- 作为对象原型链的终点(Object.prototype.proto)
- 释放内存
以上只是简单列举了一些情况。
类型断言
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
另一个为as语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
在TypeScript里使用JSX时,只有 as语法断言是被允许的。因为使用尖括号<>会被JSX解析成组件。
可索引类型接口
interface IArray {
[index: number]: string;
}
let arr: IArray= ["ts教程", "广东省"];
let ant: string = arr[0];
上面规定索引签名是number类型,返回值是字符串类型。
支持两种索引签名:字符串和数字。可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 数字索引类型“Animal”不能赋给字符串索引类型“Dog”
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
我们把接口左侧名字称为key,右侧的类型称为value。
可以这么理解,使用了字符串索引类型[x: string]的key,那么它对应的类型必须是其他key的父类类型。
正确使用:
interface NotOkay {
[x: number]: Dog;
[x: string]: Animal;
}
例子2:
interface Dictionary {
[index: string]: string;
length: number; // 错误,length是number类型
name: string // 可以,`name`的类型与索引类型返回值的类型匹配
}
先看索引类型[index: string]: string;返回的是string类型,根据上面说,字符串索引类型其他key返回的类型的父类。
- length返回的类型是number,并不是string的子类,所以错误。
- name返回的类型是string,跟索引类型返回的类型一致,所以没问题。
还可以这么理解,[index: string] 这种表述形式,算是一个统称,其实是包括了length和name这两个key,只是单独把这两个key列举出来,说明这个接口里面有这两个成员。那length和name这两个key的类型就应该跟[index: string]返回的类型至少是一致的,为string类型。
泛型
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
和普通函数的区别是,在函数签名声明的前面加上泛型类型参数。
接口中应用泛型
在接口中的成员使用泛型:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
上面接口的应用和普通函数接口区别很小,只是在函数前面添加了泛型类型变量。
在接口上增加泛型:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
上面的代码将泛型参数当做接口的一个参数传递,这样接口的应用就更加灵活。
泛型类
class Antzone<T> {
webName: T;
}
let antzone = new Antzone<string>();
antzone.webName = "蚂蚁部落";
在类名称后面传递泛型类型参数。
特别说明: 泛型类指的是实例部分的类型,类的静态属性不能使用这个泛型类型。
泛型约束
你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
这里的x变量会被推导成object类型,
如果我将x类型设置为any,会有什么区别吗?
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x: any = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m");
这个时候编译器不会报错了,因为T为any类型,K extends keyof T相当于 K extends keyof any,也是any类型,所以getProperty(x, “m”)不会报语法错误,但得到的值为undefined。
在泛型里使用类类型
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。
function create<T>(c: {new(): T; }): T {
return new c();
}
例子:
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
createInstance中的参数c类型是一个(new () => A),这个参数可以被new 一个实例,说明这个参数c并不是一个普通的类型,只能是构造函数。
在js里面是没有类的,ES6里面的class也只是个语法糖,编译后依然为一个function,也就是构造函数。(new () => A)返回时一个A的实例,所以传入的是一个class,也就是一个构造函数,为了区别普通的function,所以前面加了new关键字。
高级类型
类型别名
类型别名用来给一个类型起个新名字。
type ant = string;
let str:ant=“类型”;
类型别名和接口的区别:
通过上面的介绍,类型别名与接口有一些类似之处,但是区别也是很明显的:
- 错误信息不会使用别名。
- 接口是创建一个新的类型,别名不会创建一个新类型,是对原有类型的引用。
- 即使使用别名,编辑器只能提示还是会显示原有类型名称。
- 类型别名不能被 extends和 implements
字面量类型
字面量类型包括:字符串字面量类型和数字字面量类型,用的比较多的是字符串字面量类型。
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
type sex="男" | “女";
交叉类型
可以将现有的多种类型叠加到一起成为一种类型。
interface T1 {
a: boolean;
b: string;
}
interface T2 {
a: boolean;
b: number;
}
type T = T1 & T2;
type Ta = T['a']; // boolean
type Tb = T['b']; // string & number
上面的代码中,T1和T2接口中,b属性类型冲突,那么最终b属性类型为交叉类型string & number,同时string类型或者number类型无法赋值给交叉类型string & number,最终导致报错。为了解决这个问题,可以将resut再断言为Any类型,这样result所有属性均为Any类型,那么赋值就不会报错了。
联合类型
联合类型表示一个值可以是几种数据类型中的某一种。类型之间使用竖线"|"分隔,代码如下:
let val:number | string;
如果一个值是联合类型,那么只能访问联合类型的共有成员。
class Bird{
leg=2;
color="white";
fly(){
// code
}
}
class Fish {
leg=8;
color="black";
swim(){
// code
}
}
function union(): Bird | Fish {
return new Bird();
}
let animal = union();
animal.color;
animal.leg;
animal.eat()// 报错,只能访问共有成员
如果一个值的类型是 A | B,我们能够 确定的是它包含了 A 和 B中共有的成员。 这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 animal.eat()就出错了。
keyof
keyof是索引类型查询操作符。
假设T是一个类型,那么keyof T产生的类型是T的属性名称字符串字面量类型构成的联合类型。
特别说明:T是数据类型,并非数据本身。
interface Person {
name: string;
age: number;
}
let personProps: keyof Person; // 'name' | 'age'
typeof
typeof 代表取某个值的 type
const a: number = 3
const b: typeof a = 4
// 相当于: const b: number = 4
类型保护
四种类型:自定义的类型保护、typeof类型保护、instanceof类型保护、switch case类型保护。
自定义的类型保护
export interface AccountPersonal {
username: string;
nickname: string;
avatar: string;
}
// 'username' in obj表示 username是obj中的属性
export function isAccountPersonal(obj: any): obj is AccountPersonal {
return 'username' in obj;
}
在这个例子里, obj is AccountPersonal就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。
const personal = {
username: 'zzb';
nickname: 'binge';
avatar: 'http://baidu.com';
}
if (isAccountPersonal(personal)) {
// 通过判断则可以知道personal是属于AccountPersonal类型,这样就可以使用代码提示出类型里面的成员。
console.log(personal.username) // 输出'zzb'
}
parameterName is Type
如果函数返回值为true,那么也就意味着类型谓词是成立的,于是它后面作用域的类型也就被固定为Type。
typeof类型保护
上面介绍过typeof的用法,这里可以利用它的特性来做类型保护。
function ant(param: string | number) {
if (typeof param === "number") {
console.log(param+5); //
}
if (typeof param === "string") {
console.log(param+"蚂蚁部落");
}
}
由于应用了typeof,后面作用域的param就确定为number类型。
instanceof类型保护
interface Animal {
numLegs: number;
}
class Chicken implements Animal {
numLegs: number = 2;
eat = () => {
}
}
class Lion implements Animal {
numLegs: number = 4;
run = () => {
}
}
function getAnimal(numLegs: number = 2) {
return numLegs <= 2 ?
new Chicken() :
new Lion();
}
// 类型为chicken | Lion
let padder: Animal = getAnimal(2);
if (padder instanceof Chicken) {
padder.eat(); // 类型细化为'Chicken'
}
if (padder instanceof Lion) {
padder.run(); // 类型细化为'Lion'
}
定义:instanceof运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。
简单理解,instanceof可以判断左侧对象是否是右侧类型的子类。
switch case类型保护
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(shape: Shape) {
switch (shape.kind) {
case "square": return shape.size * shape.size;
case "rectangle": return shape.height * shape.width;
case "circle": return Math.PI * shape.radius ** 2;
}
}
Partial
// 可选类型
// 实现源码
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
id: number;
age: number;
name: string;
};
type PartialUser = Partial<User>
// 相当于: type PartialUser = { id?: number; age?: number; name?: string; }
Pick
// 挑选属性
// 实现源码
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
age: number;
name: string;
};
type PickUser = Pick<User, "id" | “age">
// 相当于: type PickUser = { id: number; age: number; }
Readonly
将一个类型转换成只读类型
// 实现源码
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
interface User {
id: number;
age: number;
name: string;
};
type ReadonlyUser = Readonly<User>
ReadonlyUser = {
readonly id: number;
readonly age: number;
readonly name: string;
}
装饰器(decorator)
装饰器只能作用在类,属性,
访问符,方法或方法参数上,这篇文章只会简单讲解类装饰器和React的高阶组件关系,装饰器详细的内容会在另一篇文章记录。
装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
简单来说,装饰器就是用一个代码包装另一个代码的简单方式,就是简单的将一个函数包装成为另一个函数。
为什么要用装饰器
我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。
所以,对于装饰器,可以简单地理解为是非侵入式的行为修改。
如何定义装饰器
装饰器本身其实就是一个函数,理论上忽略参数的话,任何函数都可以当做装饰器使用。
function helloWord(target: any) {
console.log('hello Word!');
}
@helloWord
class HelloWordClass { }
后面会讲解。
装饰器执行时机
修饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
类装饰器
当装饰函数直接修饰类的时候,装饰函数接受唯一的参数,这个参数就是该被修饰类本身。
function helloWord(target: any) {
console.log('hello Word!');
}
@helloWord
class HelloWordClass { }
普通装饰器(无法传参)
function testable(target) {
target.isTestable = true;
}
@testable
class MyTestableClass {}
console.log(MyTestableClass.isTestable) // true
装饰器工厂(可传参)
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
重载构造函数
function foo(target) {
return class extends target {
name = ‘zzb’;
sayHello(){
console.log("Hello"+this.name)
}
}
}
@foo
class P{
constructor(){ }
}
const p = new P();
p.sayHello(); // 会输出Hello zzb
React高阶组件
再看一下React高阶组件的定义
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
具体而言,高阶组件是参数为组件,返回值为新组件的函数。
通过对上面装饰器的了解,高阶组件可以看做是装饰器模式(Decorator Pattern)在React的实现。即允许向一个现有的对象添加新的功能,同时又不改变其结构。
React 中实现高阶组件的方法:属性代理(Props Proxy)和 反向继承(Inheritance Inversion)。
其中属性代理的方式就是利用装饰器模式实现。
高阶组件应用装饰器实例
选用了react的初始化项目做例子,稍微修改了一下。
定义装饰器:
export function withName(name: string): any {
return (InnerComponent: any) => {
return function NameWrapper(props: any) {
return (
<div>
<InnerComponent
name={name}
{...props}
/>
</div>
);
};
};
}
给App组件进行包装
@withName('zzb') // 关键代码
class App extends React.PureComponent<any, any>{
render() {
const {name} = this.props;
return (
<div className="App">
<header className="App-header">
<p style={{color: 'yellow'}}>通过装饰器传递进来的名字: {name}</p>
</header>
</div>
);
}
}
结果:
如果App是一个函数式组件
装饰器不能直接放在函数上方,前面说过会造成函数提升,装饰器只能作用在类,属性,访问符,方法或方法参数。想要对函数式组件进行包装,就需要以下方式
export default withName('zzb')(App); // 关键代码
完整代码:
const App: React.FC = ({name, ...props}: any) => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p style={{color: 'yellow'}}>通过装饰器传递进来的名字: {name}</p>
</header>
</div>
);
}
export default withName('zzb')(App); // 关键代码