Skip to content
On this page

泛型

前言

阅读部分:Typescript官方手册---Type Manipulation类型操作

欢迎加入ts对赌学习,本文为第一天学习总结。

TypeScript 的类型系统非常强大,因为它允许根据其他类型 来表达类型

(一)泛型

我对泛型的理解:不需要用户指定特定的类型,可以通过编程的方式,让程序自己能推导出应该是什么类型 的一种不确定的类型

我们需要的,是一种捕获参数类型的方法,以便我们也可以使用它来表示返回的内容。

举一个例子:

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作为数组元素的类型,返回一个该类型的数组,而不是该类型。

typescript
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // Array有.length,所以不会报错
  return arg;
}

通用类型

我们可以定义一个通用接口,把这个泛型抽离出来,形成通用类型

typescript
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;

通用类

泛型类和泛型接口很相似,泛型类<>在类名后面的 ( ) 中,包含了泛型类型的参数列表

typescript
class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}

你可以对NumType传出number或string或者任意类型,就像接口一样,将类型参数放在类本身上可以确保类的所有属性都使用相同的类型。

通用约束

当我们希望对用户传进来的Type有所约束时,我们可以通过extends来进行约束

typescript
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 });

在通用约束中使用类型参数

举个例子,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取到不存在的属性,因此我们将在两种类型之间放置一个约束。

typescript
//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");

在泛型中使用类类型

typescript
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 操作符提取其属性的名称。

typescript
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 也可以用于操作类,比如:

typescript
class Person {
  name: string = "Semlinker";
}

let sname: keyof Person;
sname = "name";

若把 sname = "name" 改为 sname = "age" 的话,TypeScript 编译器会提示以下错误信息:不能将类型“"age"”分配给类型“"name"”

实战场景

我们希望定义一个函数,传入一个对象和key值,返回对应value

typescript
function prop(obj, key) {
  return obj[key];
}

我们希望实现这样子的效果,该函数用于获取 某个对象中指定属性的属性值 。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof 操作符:

typescript
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 联合类型的子类型。

来源:http://semlinker.com/ts-keyof/

(三)Typeof 类型运算符

在 TypeScript 中,typeof 操作符可以用来获取一个变量或对象的类型。

typescript
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 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:

typescript
function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

来源:http://www.semlinker.com/ts-typeof/

(四)索引访问类型

我们可以使用索引访问类型来查找另一种类型的特定属性

typescript
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; //number
type Age = Person[keyof Person]; //string | number | boolean

我们可以通过 number获取数组(元组)元素的类型(联合类型):

typescript
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;
}
*/

只能用类型变量进行索引查找:

typescript
// 错误
// const key = "age";
// type Age = Person[key];

//js和ts的内容不能混用
//[]中得使用一个type变量
const key = "age";
type Age = Person[typeof key];

(五)条件类型

我们可以根据传进去的泛型T,通过逻辑判断,得到一个真实的类型:

typescript
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;

函数重载

同样的,在函数重载方面,条件类型也发挥了很大的作用:

typescript
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);

条件类型约束

typescript
//我们约束: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>;

另外一个例子,实现一下扁平化数组的功能

typescript
type Flatten<T> = T extends any[] ? T[number] : T;
 
// 当传进去是一个数组类型时,返回数组元素类型
type Str = Flatten<string[]>;

// 当传进去一个元素类型是,返回本身
type Num = Flatten<number>;

在条件类型中推断

另外,ts还给我们提供了一种用法

typescript
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

使用 infer 声明性地引入了一个新的泛型类型变量 Item,而不是指定如何在 true 分支中检索 T 的元素类型。

这样子我们可以使用infer编写一些有用的类型别名,例如,提取函数类型中的返回类型

typescript
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

当存在多个重载函数时,对最后一个函数进行推导

typescript
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>;

分配条件类型

typescript
type ToArray<Type> = Type extends any ? Type[] : never;

有这么一种情况,当Type传进一个联合类型

typescript
type StrArrOrNumArr = ToArray<string | number>;

StrArrOrNumArr 得到的结果是 string[] | number[]

如果我们想变成 (string | number)[] 类型,可以用方括号将 extends 关键字的每一边包围起来。

typescript
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

//StrArrOrNumArr : (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;

(六)映射类型

有时候我们不希望返回本身,有时候希望可以改变成为一种类型。

映射类型是一种泛型类型,它使用 PropertyKeys (通常通过 keyof 创建)的联合来迭代键以创建类型:

typescript
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? ,它们分别影响可变性和可选性。

另外,可以通过使用 -+ 作为前缀来删除或添加这些修饰符。如果没有添加前缀,则假定为 + 。

typescript
//去除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;
}
*/
typescript
//去除可选性质
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的名称进行更改:

typescript
//注意使用反引号
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;
}
*/

另一个例子

typescript
//指定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 中的模板字符串具有相同的语法,但是用于类型位置。当与具体文本类型一起使用时,模板文本通过连接内容生成一个新的字符串文本类型。

typescript
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}`;

类型字符串联合

将字符串和类型进行联合使用,将碰撞出不一样的火花。

typescript
//该类型存在一个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", () => {});

这样子我们可以在使用时,给予我们代码提示,这是我们希望的。

image-20211120162107337

image-20211120162126468

内部字符串操作类型

typescript
Uppercase<StringType> //转大写
Lowercase<StringType> //转小写
Capitalize<StringType> //首字母大写
Uncapitalize<StringType>//首字母小写

至此,这部分的内容就学完啦~可以摸会鱼了。

持续更新中~欢迎关注我的掘金和github,觉得不错的话,记得给我的项目🌟 一下哦~

MIT Licensed | Copyright © 2021 - 2022