一、泛型介绍
在实际应用中,我们不仅需要创建定义良好的API,还需要考虑复用性。组件不仅需要支持当前的数据类型,还需要考虑未来的数据类型。
在像 C# 和 Java 这样的语言中,可以利用泛型来创建可复用的组件,这样一个组件可以支持多种数据类型。
首先用一个简单的例子介绍泛型:
function identity(value:Number):Number{
return value
}
function identity(value:any):any{
return value
}
console.log(identity(1)) // 1
如果将参数和返回值都定义成Number类型,后期将无法使用其他数据类型。都定义成any,那么则失去了定义哪种类型返回值的能力。使用泛型则可以解决这个问题,约束函数参数和返回值的类型。
function identity<T>(value:T):T{
return value
}
console.log(identity<Number>(1)) // 1
console.log(identity<String>('a')) // a
二、泛型语法
可以将理解为传递参数,调用identity函数时,传递类型Number或者其他类型。其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
另外,我们不止可以定义一种类型。
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
调用函数时,可以不使用<>传递类型,让编译器自动识别类型。
console.log(identity(68, "Semlinker"));
如果在函数中操作的是数组,而不是T,这样更直观地理解T是类型变量。
function identity<T>(arg:Array<T>):Array<T>{
console.log(arg.length);
return arg;
}
上述的例子,都是返回一个类型的值,如果需要返回两个类型的值,该怎么处理呢?其中一种方式是使用元组:
function identity<T,U>(value:T,message:U):[T,U]{
return [value,message]
}
三、泛型接口
接口可以用来形容对象的形状,上述例子需要返回两个类型的值,则可以通过泛型接口的形式来定义返回类型。
interface identities<T,U>{
value:T,
message:U
}
function identity<T,U>(value:T,message:U):identities<T,U>{
return {value,message}
}
console.log(identity('jucy', "Hello")); //{ value: 'jucy', message: 'Hello' }
四、泛型类
首先先复习一下class:
class Student{
scope:100; //实例属性,有默认值
constructor(name,age){
this.name = name;
this.age = age
} //实例化后传参
getName() {
return this.name
}
}
const s = new Student('jucy',18)
console.log(s.getName)
属性写在构造函数上(不占用内存),new实例调用后,this指向实例,因此实例具有name、age、scope三个属性。而getName是原型对象上的属性,实例的_proto_属性会指向prototype,因此可以通过该指针找到原型上的方法(如果方法直接写在实例属性上,那么将会占用内存空间,因此属性放在constructor里,方法一般放在原型对象上)。
ts写法:
interface GenericInterface<U> {
value: U
getIdentity: () => U
}
class IdentityClass<T> implements GenericInterface<T> {
value: T
constructor(value: T) {
this.value = value
}
getIdentity(): T {
return this.value
}
}
const myNumberClass = new IdentityClass<Number>(68);
console.log(myNumberClass.getIdentity()); // 68
const myStringClass = new IdentityClass<string>("Semlinker!");
console.log(myStringClass.getIdentity()); // Semlinker!
类IdentityClass实现了泛型接口GenericInterface,类传入类型变量T,再赋给泛型接口和其余变量。
五、泛型约束
5.1确保属性存在
我们需要访问变量的属性,但是类型却不能证明含有该属性,将会报错。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
此时需要对类型T作约束,创建一个包含.length属性的接口,使用这个接口和extends关键字来实现约束:
interface LengthWise {
length:number
}
function loggingIdentity<T extends LengthWise>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
现在这个泛型函数被定义了约束,因此对它不再适用于任何类型,需要传入含有length属性的值。
5.2 确保键是否存在
先了解一下keyof操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,用于获取某种类型的所有key值,其返回值是联合类型。在JavaScript中我们想要获取一个对象的key可以通过Object.keys(obj)来获取。
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number
通过keyof可以获取某个类型的所有key值,使用extends约束,即可限制输入的属性名在keyof返回的键值联合类型中。(类型A extends 类型B后,也可以自定义新增属性,不新增则等同于类型B)
function getProperty<V,K extends keyof V>(value:V,key:K):V[K]{
return value[key]
}
let person = {
name:'jucy',
age:12,
scope:100
}
console.log(getProperty(person,'name')) //jucy
console.log(getProperty(person,'class')) //error
很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。
六、泛型工具类型
6.1 Partial
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。
Partial的作用是将某个类型的所有属性变成可选的,传入类型T(对象等),定义:
type Partial<T> = {
[P in keyof T]?:T[P]
}
在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。
interface Car {
color:string,
pride:number
}
function createObj(obj:Partial<Car>){
return obj
}
console.log(createObj({color:'green'}))
将接口Car中的属性都变成可选的。
interface Car {
color?:string,
pride?:number
}
6.2 Record
Record<K,T>的作用是将K(联合类型)对应的所有属性全部转换为T类型。定义:
type Record<K extends keyof any,T> = {
[P in K]:T
}
type Animal = 'dog'|'cat'|'monkey'
interface AnimalProperty{
name:string,
age:number
}
const animals:Record<Animal,AnimalProperty> = {
dog:{
name:'wangwang',
age:12
},
cat:{
name:'cat',
age:12
},
monkey:{
name:'jerry',
age:12
}
}
console.log(animals)
//如果需要将animals的属性变成可选
const animals:Partial<Record<Animal,AnimalProperty>> = {
dog:{
name:'wangwang',
age:12
},
cat:{
name:'cat',
age:12
}
}
6.3 Pick
从一个类型中挑出一个类型或者联合类型,作为一个新的类型。英文定义:Constructs a type by picking the set of properties Keys (string literal or union of string literals) from Type.<Type,Keys>传入类型和挑选出的key。
定义:
type Pick<T,K extends keyof T> = {
//遍历K键值
[P in K]:T[P]
}
interface PageInfo {
title:string,
link:string,
origin:string
}
let subPageInfo:Pick<PageInfo,'title'|'link'> = {
title:"detail",
link: "/detail",
}
console.log(subPageInfo)
6.4 Exclude
Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。
定义:
type Exclude<T, U> = T extends U ? never : T
当 T 是联合类型时,则会循环 T 类型即: (T1 extends U ? never : T1) | (T2 extends U ? never : T2) | … 如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是
将 T 中某些属于 U 的类型移除掉。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
6.5 ReturnType
ReturnType 的作用是用于获取函数 T 的返回类型。 示例:
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
type T6 = ReturnType<string>; // Error
type T7 = ReturnType<Function>; // Error