泛型
前言
阅读部分:Typescript官方手册---Type Manipulation类型操作
欢迎加入ts对赌学习,本文为第一天学习总结。
TypeScript 的类型系统非常强大,因为它允许根据其他类型 来表达类型。
(一)泛型
我对泛型的理解:不需要用户指定特定的类型,可以通过编程的方式,让程序自己能推导出应该是什么类型 的一种不确定的类型。
我们需要的,是一种捕获参数类型的方法,以便我们也可以使用它来表示返回的内容。
举一个例子:
//我们使用一个泛型Type
function identity<Type>(arg: Type): Type {
return arg;
}
//当我们调用是传入Type的类型应该是number时
//程序会规定arg以及函数返回的类型为number
let res = identity<number>(1);
//如果不显式地指定类型,编译器也会进行推断
let res = identity(1);
在Typescript中,允许我们将泛型类型变量作为我们使用类型的一部分,而不是整一个类型,提供了更大的方便性,例如,我们可以把Type作为数组元素的类型,返回一个该类型的数组,而不是该类型。
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array有.length,所以不会报错
return arg;
}
通用类型
我们可以定义一个通用接口,把这个泛型抽离出来,形成通用类型
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
//使Type可以看成是整个接口的参数
//使得类型参数对接口的所有其他成员可见
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
通用类
泛型类和泛型接口很相似,泛型类<>
在类名后面的 ( ) 中,包含了泛型类型的参数列表
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
你可以对NumType传出number或string或者任意类型,就像接口一样,将类型参数放在类本身上可以确保类的所有属性都使用相同的类型。
通用约束
当我们希望对用户传进来的Type有所约束时,我们可以通过extends来进行约束
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length);
return arg;
}
//类型“number”不满足约束“Lengthwise”。
loggingIdentity<number>(3);
//ok
loggingIdentity({ length: 10, value: 3 });
在通用约束中使用类型参数
举个例子,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取到不存在的属性,因此我们将在两种类型之间放置一个约束。
//Key extends keyof Type
//代表传入的key必须包含在Type说返回的所有key之中
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
//ok
getProperty(x, "a");
//error,类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数
getProperty(x, "m");
在泛型中使用类类型
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
(二)Keyof 类型运算符
TypeScript 允许我们遍历某种类型的属性,并通过 keyof 操作符提取其属性的名称。
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 也可以用于操作类,比如:
class Person {
name: string = "Semlinker";
}
let sname: keyof Person;
sname = "name";
若把 sname = "name"
改为 sname = "age"
的话,TypeScript 编译器会提示以下错误信息:不能将类型“"age"”分配给类型“"name"”
实战场景
我们希望定义一个函数,传入一个对象和key值,返回对应value
function prop(obj, key) {
return obj[key];
}
我们希望实现这样子的效果,该函数用于获取 某个对象中指定属性的属性值 。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof
操作符:
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
首先定义了 T 类型并使用 extends
关键字约束该类型必须是 object 类型的子类型,然后使用 keyof
操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends
关键字约束 K 类型必须为 keyof T
联合类型的子类型。
(三)Typeof 类型运算符
在 TypeScript 中,typeof
操作符可以用来获取一个变量或对象的类型。
let s = "hello";
let n: typeof s;
//n:string
const kakuqo = {
name: "kakuqo",
age: 30,
address: {
province: '福建',
city: '厦门'
}
}
type Kakuqo = typeof kakuqo;
/*
type Kakuqo = {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
*/
此外,typeof
操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
(四)索引访问类型
我们可以使用索引访问类型来查找另一种类型的特定属性
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; //number
type Age = Person[keyof Person]; //string | number | boolean
我们可以通过 number
获取数组(元组)元素的类型(联合类型):
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person = typeof MyArray[number];
/*
type Person = {
name: string;
age: number;
}
*/
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", height: "173cm" },
{ name: "Eve", age: 38 },
];
type Person = typeof MyArray[number];
/*
type Person = {
name: string;
age: number;
height?: undefined;
} | {
name: string;
height: string;
age?: undefined;
}
*/
只能用类型变量进行索引查找:
// 错误
// const key = "age";
// type Age = Person[key];
//js和ts的内容不能混用
//[]中得使用一个type变量
const key = "age";
type Age = Person[typeof key];
(五)条件类型
我们可以根据传进去的泛型T,通过逻辑判断,得到一个真实的类型:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// Example1 : number
type Example1 = Dog extends Animal ? number : string;
// Example2 : string
type Example2 = RegExp extends Animal ? number : string;
函数重载
同样的,在函数重载方面,条件类型也发挥了很大的作用:
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
//不使用泛型,不够简洁,需要写出所有情况
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
//泛型条件判断,如果T是number那么NameOrId就是IdLabel类型
//反之是NameLabel类型
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
//以此为例子
//该函数接受number或string类型的参数,对应的返回值是IdLabel或者NameLabel
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
let b = createLabel(2.8);
let c = createLabel(Math.random() ? "hello" : 42);
条件类型约束
//我们约束:T必须包含一个名为message的属性,该类型返回message的类型
//其实完成了两件事:一件事是对传入T的约束,一件事是对自身类型的控制
type MessageOf<T extends { message: unknown }> =? T["message"] : never;
interface Email {
message: string;
}
//EmailMessageContents : string
type EmailMessageContents = MessageOf<Email>;
interface Dog {
bark(): void;
}
//DogMessageContents : never 因为Dog接口中未包含message这个属性
type DogMessageContents = MessageOf<Dog>;
另外一个例子,实现一下扁平化数组的功能
type Flatten<T> = T extends any[] ? T[number] : T;
// 当传进去是一个数组类型时,返回数组元素类型
type Str = Flatten<string[]>;
// 当传进去一个元素类型是,返回本身
type Num = Flatten<number>;
在条件类型中推断
另外,ts还给我们提供了一种用法
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
使用 infer
声明性地引入了一个新的泛型类型变量 Item,而不是指定如何在 true 分支中检索 T 的元素类型。
这样子我们可以使用infer编写一些有用的类型别名,例如,提取函数类型中的返回类型
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
当存在多个重载函数时,对最后一个函数进行推导
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: string | number): string | number;
declare function stringOrNum(x: number): string;
//T1 : string
type T1 = ReturnType<typeof stringOrNum>;
分配条件类型
type ToArray<Type> = Type extends any ? Type[] : never;
有这么一种情况,当Type传进一个联合类型
type StrArrOrNumArr = ToArray<string | number>;
StrArrOrNumArr
得到的结果是 string[] | number[]
如果我们想变成 (string | number)[] 类型,可以用方括号将 extends 关键字的每一边包围起来。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
//StrArrOrNumArr : (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;
(六)映射类型
有时候我们不希望返回本身,有时候希望可以改变成为一种类型。
映射类型是一种泛型类型,它使用 PropertyKeys (通常通过 keyof 创建)的联合来迭代键以创建类型:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
//将所有属性均变为boolean类型
type FeatureOptions = OptionsFlags<FeatureFlags>;
/*
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/
映射修饰符
在映射过程中可以使用两个额外的修饰符: readonly
和?
,它们分别影响可变性和可选性。
另外,可以通过使用 -
或 +
作为前缀来删除或添加这些修饰符。如果没有添加前缀,则假定为 + 。
//去除readonly
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
/*
UnlockedAccount : {
id: string;
name: string;
}
*/
//去除可选性质
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
/*
type User = {
id: string;
name: string;
age: number;
}
*/
使用as对Key重新映射
可以使用映射类型中的 as 子句重新映射映射类型的键,例如对key的名称进行更改:
//注意使用反引号
type Getters<Type> = {
[Property in keyof Type as /`get${Capitalize<string & Property>}/`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
另一个例子
//指定Type中属性kind的值作为新的属性名
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>
/*
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}
*/
(七)模板文字类型
模板文字类型建立在字符串文字类型之上,并且能够通过联合扩展成许多字符串。
与 JavaScript 中的模板字符串具有相同的语法,但是用于类型位置。当与具体文本类型一起使用时,模板文本通过连接内容生成一个新的字符串文本类型。
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
//type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
//对于模板文字中的每个插值位置,联合是十字乘:
type Lang = "en" | "ja" | "pt";
//type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
类型字符串联合
将字符串和类型进行联合使用,将碰撞出不一样的火花。
//该类型存在一个on方法,名称为每一个属性名+Changed
type PropEventSource<Type> = {
on(
eventName: `${string & keyof Type}Changed`,
callback: (newValue: Type[Key]) => void
): void;
};
//我们声明一个方法,将原类型和上面的类型合并(&)起来
declare function makeWatchedObject<Type>(
obj: Type
): Type & PropEventSource<Type>;
//创建一个实例
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
这样子我们可以在使用时,给予我们代码提示,这是我们希望的。
内部字符串操作类型
Uppercase<StringType> //转大写
Lowercase<StringType> //转小写
Capitalize<StringType> //首字母大写
Uncapitalize<StringType>//首字母小写
至此,这部分的内容就学完啦~可以摸会鱼了。
持续更新中~欢迎关注我的掘金和github,觉得不错的话,记得给我的项目🌟 一下哦~