🚀🚀🚀Typescript通关秘籍(五)🔥🔥🔥

Class

class 的基本语法我们就不探究了,classTS 中是全面支持的,我们主要关注 TS 对于类属性和方法的类型声明方式即可。

对于类属性,有两种声明类型的方式:

class Person {
  name: string; // 1. 直接在顶层声明时顺便给出类型(推荐)
  age: number = 18; // 2. 通过类型推断
  constructor(name: string) {
    this.name = name;
  }
}

类属性在 TS 中默认要求有初始值,也可以通过 strictPropertyInitialization 配置项去关闭。

对于类方法,它与普通函数的类型声明方式一致。

class Person {
  name = '橙某人';
  constructor(name: string) {
    this.name = name;
  }
  say(newName: string): void {
    console.log(`my name is ${this.name}`);
  }
}

implements

implements 表示实现,一个新类必须实现类型别名、接口或者父类的所有属性和方法。

type P = {
  name: string;
  age: number;
  say: (newName: string) => void;
}

/* or
interface P {
  name: string;
  age: number;
  say: (newName: string) => void;
}
*/

class Person implements P {
  name;
  age = 18;
  constructor(name: string) {
    this.name = name;
  }
  say(newName: string) {
    console.log(`my name is ${this.name}`);
  }
}

需要注意,类方法参数的类型声明不能省略,也不能改成其他类型,否则参数类型会变成 any 类型或者报错。😕

class Person implements P {
  say(newName) { // any
  }
}
class Person implements P {
  say(newName: number) { // ❌
  }
}

多重实现

类可以实现多重限制,每个限制之间使用逗号隔开。

type P1 = {
  name: string;
}
interface P2 {
  age: number;
}
class P3 {
  say(newName: string): void {}
}

class Person implements P1, P2, P3 {
  name = '橙某人';
  age = 18;
  say(newName: string) {
    console.log(`my name is ${this.name}`);
  }
}

实现多重限制时,不同限制之间不能有冲突的属性。

实例类型声明

声明类实例的类型有两种方式:

type P1 = {
  name: string;
}
interface P2 {
  age: number;
}
class P3 {
  say(newName: string): void {}
}

class Person implements P1, P2, P3 {
  name = '橙某人';
  age = 18;
  say(newName: string) {
    console.log(`my name is ${this.name}`);
  }
}

let p1: Person = new Person(); // 推荐
let p2: P1 = new Person();

p2.name // ✅
p2.age // ❌

修饰符

readonly

readonlyTS 新增的一个关键字,表示只读,不可能被修改。

它的出现主要是为了解决 const 声明的对象,所指堆空间对象依旧可变的不足。

列了一些使用场景:

// 对象
let obj: {
  readonly name: string;
  readonly age: number;
} = {
  name: '橙某人',
  age: 18
}
obj.name = ''; // ❌


// 函数
function fn(obj: { readonly name: string; readonly age: number; }) {
  obj.name = ''; // ❌
}

// 数组
let arr: readonly number[] = [1, 2, 3];
arr.push(4); // ❌ 

// 元组
let tArr: readonly [number, string] = [1, 'a'];
tArr.push(4); // ❌

// type
type Obj = {
  readonly name: string;
  readonly age: number;
}
let o: Obj = {
  name: '橙某人',
  age: 18
}
o.name = ''; // ❌

// interface
interface Person {
  readonly name: string;
  readonly age: number;
}
let oo: Person = {
  name: '橙某人',
  age: 18,
}
oo.name = ''; // ❌

// class
class Student {
  readonly name: string = '橙某人';
  readonly age: number;
  constructor() {
    this.age = 18; // 这里只是初始化而已,不是修改❗
  }
  say() {
    this.name = ''; // ❌
  }
}

随便看看就得了。👻

泛型

泛型(Generics)可以理解成一个占位的未知”类型参数”,具体这个类型参数是什么类型需要在使用的时候才能确定下来。

来看个例子:

function numToArray(a: number, b: number): number[] {
  return [a, b];
}
function strToArray(a: string, b: string): string[] {
  return [a, b];
}

let numArray = numToArray(1, 2); // number[]
let strArray = strToArray('a', 'b'); // string[]

上述两个函数,在 JS 中完全能被写成一个,但是在 TS 中却需要拆成两个函数,只因参数和返回值类型的不一致导致。

虽然也能用函数重载来解决😏,但…这不重要不重要。

而这种场景,利用泛型就能很好得到解决。

function fn<T>(a: T, b: T): T[] {
  return [a, b];
}

let numArray = fn<number>(1, 2); // number[]
let strArray = fn<string>('a', 'b'); // string[]

是不是能感受到泛型的灵活和强大了?💪

当然,泛型的类型参数可以有多个,通过逗号隔开。

function fn<T, K, V>(a: T, b: K, c: V): (T | K | V)[] {
  return [a, b, c];
}

let res = fn<number, string, boolean>(1, 'a', true); // (string | number | boolean)[]

T, K, V 这些参数命名与函数参数名一样,可以由你随意定义。

四种应用场景

泛型主要用在四个场景:函数、类型别名、接口、class

函数

function fn1<T>(args: T): T {
  return args;
}
fn1<number>(1);

let fn2: <T>(args: T) => T = args => args;
fn2<number>(1);

let obj: {
  fn3: <T>(args: T) => T;
} = {
  fn3: args => args
};
obj.fn3<number>(1);

类型别名

type ArticleName<T> = string | number | T;
type Person<T, K> = {
  name: T;
  age: K;
}

let an1: ArticleName<string> = '橙某人';
let an2: ArticleName<number> = 999;
let an3: ArticleName<boolean> = true;

接口

interface Person<T, K> {
  name: T;
  age: K;
}

let person: Person<string, number> = {
  name: '橙某人',
  age: 18
}

函数的泛型接口可以有两种形式:

// 在定义时传递类型
interface Fn1<T> {
  (args: T): T;
}
let fn4: Fn1<number> = args => args;
fn4(1);

// 在调用函数的时候传递类型
interface Fn2 {
  <T>(args: T): T;
}
let fn5: Fn2 = args => args;
fn5<number>(1);

class

class Person<T, K> {
  name: T;
  constructor(name: T) {
    this.name = name;
  }
  say(args: K): K {
    return args;
  }
}

let p = new Person<string, number>('橙某人');

默认值

泛型的类型参数也可以设置默认值,当我们没有传递类型时,就会使用默认值。

type ArticleName<T = string> = T;

let name1: ArticleName<number> = 1; // number
let name2: ArticleName; // string

默认值有两个注意点:

  • 默认值经常会被类型推断给覆盖。
  • 多个泛型的类型参数,具有默认值的类型参数必须放置到最后面。

用上面的例子来看:

function fn<T = string>(a: T, b: T): T[] {
  return [a, b];
}

let numArray = fn(1, 2); // number[]
let strArray = fn('a', 'b'); // string[]

它也能简写,不传类型,让 TS 自行去进行类型推断,结果是一样的。

同时,虽然没有传递类型,但是默认值(T = string)也没有生效,说明类型推断的优先级是高于默认值的。

如果有多个类型参数,具有默认值的类型参数必须在最后面。

function fn1<T = string, U>(){} // ❌

function fn2<U, T = string>(){} // ✅
function fn3<U, T = string, K = number>(){} // ✅

泛型约束-extends

泛型的强大和灵活相信你能有所体会,但是有利就有弊。

来看下面的例子:

🚀🚀🚀Typescript通关秘籍(五)🔥🔥🔥

从入参我们可以明确知道参数会有 length 属性,但是我们想打印它却不行❗

泛型约束 的存在就是为了解决这类问题的。

泛型约束使用 extends 关键字来完成,它能帮我们提前告诉 TS,泛型的类型参数可能存在”某些类型”。

function fn<T extends { length }>(args: T) {
  console.log(args.length); // ✅
}
fn<string>('abc');
fn<number[]>([1, 2, 3]);

fn<number>(123); // ❌

或者

interface Len{
  length: number
}
function fn<T extends Len>(args: T) {
  console.log(args.length); // ✅
}
fn<string>('abc');
fn<number[]>([1, 2, 3]);

fn<number>(123); // ❌

再多列几个例子体验体验:

// 1
type Name<T extends string> = T;
let name1: Name<string> = '橙某人';
let name2: Name<number> = 1; // ❌

// 2
type Obj<T extends { name: string }> = T;
let obj1: Obj<{ name: string, age: number }> = {
  name: '橙某人',
  age: 18,
};
let obj2: Obj<{ age: number }> = {}; // ❌

// 3
type Obj<T extends U, U> = T;
let obj3: Obj<{ name: string, age: 18 }, { name: string }> = {
  name: '橙某人',
  age: 18,
}
let obj4: Obj<{ name: string }, { name: string, age: 18 }> = {} // ❌

从这些例子中,能大概瞧出(T extends UT 必须要满足 U 才行,否则就会报错。

泛型约束在一些地方也被叫做泛型条件类型,因为它和我们熟知的三元表达式(?:)很相像。

泛型约束这个概念不管在官网还是在网上的一些文章中,小编都没有一个很好的总结,大部分都是通过例子来说明。如您有更好的说明,希望、期待你在评论区中告诉小编,感谢感谢。😀😀😀

内置的泛型工具类型

泛型虽好,但不可贪多。

泛型虽然强大灵活,但是会加大代码的复杂性,可读性大大降低。

同时,泛型的编写难度门槛也是相当之高。

为此,TS 专门内置了一些常用的泛型工具类型,就像我们常用的 JS 工具方法一样。

Partial

Partial<T>:将声明的对象类型所有属性变成可选的。

示例

interface Person {
  name: string;
  age: number;
}
type P = Partial<Person>;

// 等价于
type P = {
  name?: string;
  age?: number;
}

源码实现

type Partial<T> = {
  [P in keyof T]?: T[P];
};

Required

Required<T>:将声明的对象类型所有属性变成必填的。

示例

interface Person {
  name?: string;  
  age?: number;
}
type P = Required<Person>;

// 等价于
type P = {
  name: string;
  age: number;
}

源码实现

type Required<T> = {
  [P in keyof T]-?: T[P];
};

Readonly

Readonly<T>:将声明的对象类型所有属性变成只读的。

示例

interface Person {
  name: string;  
  age?: number;
}
type P = Readonly<Person>;

// 等价于
type P = {  
  readonly name: string;  
  readonly age?: number;  
}

源码实现

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Pick

Pick<T, K>:从一个声明好的对象类型中,挑取部分属性出来组成一个新的对象类型。

示例

interface Person {
  readonly name: string;  
  age?: number;
  sex: boolean
}
type P = Pick<Person, 'name' | 'age'>;

// 等价于
type P = {  
  readonly name: string;  
  age?: number; 
}

源码实现

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Omit

Omit<T, K>:从一个声明好的对象类型中,去除部分属性,剩下的组成一个新的对象类型。(与 Pick 相反)

示例

interface Person {
  name: string;  
  age?: number;
  sex: boolean
}
type P = Omit<Person, 'name' | 'sex'>;

// 等价于
type P = {  
  age?: number;  
}

源码实现

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Record

Record<K, T>:将所有的 K 属性变成 T 类型,返回一个对象类型。

K 属性只能是 string | number | symbol

示例

type Person = 'name' | 'age';
type P = Record<Person, string>;

// 等价于
type P = {  
 name: string;  
 age: string;  
}

源码实现

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Exclude

Exclude<T, U>:将 T 类型中属于 U 类型的部分移除。

示例

type Person1 = string | number | boolean;
type Person2 = string;
type P = Exclude<Person1, Person2>;

// 等价于
type P = number | boolean;

值类型

type Person1 = '橙某人' | 18 | true;
type Person2 = '橙某人';
type P = Exclude<Person1, Person2>;

// 等价于
type P = 18 | true;

源码实现

type Exclude<T, U> = T extends U ? never : T;

😮这个其实挺有意思的,从源码中完全看不出是这个效果呢。。。猜想应该是 never 的效果,还没去细细探究过,小编期待你的评论。😖

Extract

Extract<T, U>:将 T 类型与 U 类型中相同部分提取出来,组成一个新类型。(与 Exclude 相反)

示例

type Person1 = string | number | boolean;
type Person2 = string;
type P = Extract<Person1, Person2>;

// 等价于
type P = string;

源码实现

type Extract<T, U> = T extends U ? T : never;

ReturnType

ReturnType<T>:获取函数类型的返回值类型。

示例

type Fn = (args: number) => string;
type P = ReturnType<Fn>; // string

type P1 = ReturnType<() => string>; // string
type P2 = ReturnType<() => void>; // void
type P3 = ReturnType<<T>() => T>; // unknown
type P4 = ReturnType<any>; // any
type P5 = ReturnType<never>; // never
type P6 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]

源码实现

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

Parameters

Parameters<T>:获取函数类型的参数类型,返回一个元组。

示例

type Fn = (a: number, b: string) => void;
type P = Parameters<Fn>;

// 等价于
type P = [a: number, b: string]

源码实现

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

以上小编列举了常见的十个泛型工具类型以供参考。

更多工具类型可以上源码中查阅。传送门

一些高级的工具类型的解析可以参考这篇文章。传送门


至此,本篇文章就写完啦,撒花撒花。

🚀🚀🚀Typescript通关秘籍(五)🔥🔥🔥

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

原文链接:https://juejin.cn/post/7313380227160719371 作者:橙某人

(0)
上一篇 2023年12月18日 上午10:28
下一篇 2023年12月18日 上午10:39

相关推荐

发表回复

登录后才能评论