TypeScript高级进阶

本文详细介绍了TypeScript中的关键概念,包括typeof和instanceof的区别,keyof用于获取对象类型的键,extends在泛型约束中的应用,以及this关键字在函数参数中的用法。还讨论了new关键字、infer的类型推断以及Partial和Required类型转换。此外,文章还涉及了Exclude、Extract、Pick、Omit等内置类型以及在React中的ReactNode和ReactElement类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

陆陆续续从文档上手TypeScript,遂使用本文来记录我的 TypeScript 笔记。

没有太多废话,每个 TS 中我觉得难以理解的点都会在文章中详细讲解。

无论是是原理还是实践我都会和大家一一道来,希望可以帮助到各位。

容易忽略的关键字

typeof 关键字

TypeScript 中可以使用 typeof 关键字作为类型保护,同样的还存在 instanceof 、 in 等关键字。

他们的用法比较简单,我就不过多累赘了。不了解的同学可以移步官网

let a: Person; // Person表示类的实例类型
a.yourName;
let b: typeof Person; // typeof Person 表示类的类类型
b.myName;
let c: { new (): Person } = Person; // c为构造函数类型,c拥有一个构造函数,也就是new c() 返回的是Person的实例。表示c是Person类。
复制代码

ts中通过typeof 类可以获得类的类类型,直接使用类作为类型此时使用的是类的实例类型。

keyof

The keyof operator takes an object type and produces a string or numeric literal union of its keys

keyof操作符会将一个对象类型(注意这里是类型并不是值)的key组成联合类型返回。

interface IProps {
  name: string;
  count: number;
}
type Ikea = keyof IProps; // Ikea = 'name' | 'count'

function testKeyof(props: Ikea): void { }
复制代码

extends

定义

Tsextends除了用在继承上,还可以表达泛型约束,通过extends关键字可以约束泛型具有某些属性。

其实extends关键字表示约束的时候,就是表示要求泛型上必须实现(包含)约束的属性。

示例

比如

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length) // Ts语法错误 T可以是任意类型,并不存在length属性
  return arg
}
复制代码

我们定义一个接口来描述约束条件,创建一个包含 .length 属性的接口,使用这个接口和 extends 关键字来实现约束:

interface Lengthwise {
  length: number
}

// 表示传入的泛型T接受Lengthwise的约束
// T必须实现Lengthwise 换句话说 Lengthwise这个类型是完全可以赋给T
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // OK
  return arg
}
复制代码

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error
复制代码

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3}) // OK
复制代码

日常用法

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 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
复制代码

表示传入的两个参数,第二个参数被约束成为只能传入objkey类型。

同样 extends 还可以用在类型判断语句上,比如这样:

type Env<T> = T extends 'production' ? 'production' : 'development';
复制代码

this 关键字

在 TypeScript 函数的参数上同样会存在一个 this 的关键字,假使我们需要为一个函数定义它的 this 类型。

我们可以这样做:

  • 在函数的参数上,使用 this 关键字命名,注意这里必须被命名为 this 。

  • 同样 this 必须放在函数第一个参数位上。

比如这样:

// 我希望函数中的 this 指向 { name:'19Qingfeng' }
type ThisPointer = { name: '19Qingfeng' };
function counter(this: ThisPointer, age: number) {
  console.log(this.name); // 此时TS会推断出this的类型为 ThisPointer
}
复制代码

new 关键字

new 关键字用在类型上,表示构造函数的类型。ts中 new() 表示构造函数类型。

当我们声明一个类的时候,其实声明的是这个类的实例类型和静态类型两个类型。

class Star {
  constructor() {
    // init somethings
  }
}

// 此时这里的example参数它的类型为 Star 类类型而非实例类型
// 它表示接受一个构造函数 这个构造函数new后会返回Star的实例类型
function counter(example: new () => Star) {
  // do something
}

// 直接传入类
counter(Star)
复制代码

infer

定义

infer表示在 extends 条件语句中待推断的类型变量,必须联合extends类型出现。

示例

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


interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void;

type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string
复制代码

其实碰到infer关键字简单的将它理解为一个等待推断的类型(我现在不知道,得根据条件( extends )来判断)就可以了。

重点是:

  1. infer跟随extends成双出现。
  2. infer P表示类型P是一个待推断的类型。(不使用infer直接P会报错)

内置类型

Partial

用法含义

将类型定义的所有属性都修改为可选。

type Coord = Partial<Record<'x' | 'y', number>>;

// 等同于
type Coord = {
	x?: number;
	y?: number;
}
复制代码

Partial 原理

Partial 的实现非常简单:

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

可以看出 TS 源码中这样定义 Partial :它接受传入的一个泛型类型 T ,使用 in 关键字遍历传入的 T 类型,重新定义了一个相同的类型,不过新的类型所有的 key 变成了可选类型。

需要额外注意的是当使用 Partial 时仅仅会将第一层变为可选,当存在多层嵌套时并不会递归转化。

比如这样:

此时如果我们希望实现递归可选类型,在掌握了原理之后实现起来也非常简单:

interface Person {
  name: string;
  age: number;
  detail: {
    school: string;
    company: string;
  };
}

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type PartialPerson = DeepPartial<Person>;

const person1: PartialPerson = {
  name: '19Qingfeng',
  age: 24,
  detail: {
    company: 'Tencent',
    // 即使我们不输入school也无关紧要
  },
};
复制代码

Required

用法含义

同样 Required 表示将传入的类型中的属性全部转为必选,它和 Parital 正好相反,同样不支持嵌套。

可以看到它仅仅会将 Person 的第一层属性转化为必填属性,对于嵌套属性它并不会处理。

原理实现

接下来我们去看看 Required 在 TS 中是如何实现的:

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

我在第一次接触到这个类型源码中的定义时也傻眼了,竟然还有 -? 这样的语法...没错,Required 中正是通过在属性名后使用 -? 定义属性为必填的。

至于它仅会处理一层的问题,也同样可以使用递归的方式来处理:

type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};

复制代码

Exclude<T,U>

用法含义

Exclude是进行排除 T 类型中满足 U 的类型从而返回新的类型,相对于下面的Omit操作符来说Omit是针对于key&value/接口形式的,而Exclude是针对于联合类型来操作的。

let a: string | number;

type CustomType = Exclude<typeof a, string>; // number类型
复制代码

额外注意 Exclude、Extract 可以看作是针对普通类型进行排除/提取的,而 Omit/Pick 则是针对对象类型的。

原理实现

Exclude 的原理同样非常简单,需要注意因为这里通常我们在使用 Exclude 时会在泛型中传入联合类型,并且配和 extends 关键字在条件判断语法中使用。

此时 Exclude 中的传入的联合类型会产生分发效应,而非联合类型的效果。

type Exclude<T, U> = T extends U ? never : T
复制代码

简单来说 比如我们传入

type Name = Exclude<string | number | boolean, number>
复制代码

此时我们会传入 string | number | boolean 的联合类型,所谓的分发即代表在 Exclude 源码定义中会将传入的联合类型,这里我们传入了三种,依次带入判断而非统一作为联合类型判读。

此时他会,首先将 string 传入:

string extends number ? never : T
复制代码

其他两个 number 、 boolean 会同理依次带入判断,从而返回一个满足的类型返回成为一个新的类型。

所谓分发效果,需要满足以下条件

  • 首先仅仅在泛型中传入联合类型时,是第一个需要满足的条件。

  • 其次,并且需要在条件判断语句中,简单来说 extends 语句中才会有可能产生分发。

  • 最后,必须为裸类型。

前两点在 Exclude 的源码中已经说到了,关于什么是裸类型。简单来说:

type Exclude<T, U> = T extends U ? never : T
复制代码

此时的 T 就是裸类型,当外部这样使用时:

type Name = Exclude<string | number | boolean, number>
复制代码

会满足分发的三个条件,则会产生分发效用。

但是如果 Exclude 类型定义为这样:

type Exclude<T, U> = (T & {}) extends U ? never : T
复制代码

此时的 T 并不是裸类型,我们对于泛型参数 T 进行了操作它并不是裸类型了。你可以将孤零零的 T 理解为裸类型,只有将裸类型满足前两个条件才会产生分发。

你可以这样试试:

// 这里我们定义的 CustomExclude 类型和Exclude的实现基本一致,只不过区别为 T 并非裸类型
type CustomExclude<T, U> = T & {} extends U ? never : T;

type Name = CustomExclude<string | number | boolean, number | string>;

// 此时 type Name = string | number | boolean
复制代码

因为没有产生分发的效果,固然它将传入的联合类型当作了联合类型进行判断自然是会满足于约束,所以原封不动的返回。

Extract<T,U>

用法含义

Extract 类型和 Exclude 是相反的含义,它会挑选出 T 中 符合 U 的类型,而非排除。

源码实现

它的源码中同样利用了分发的概念:

type Extract<T, U> = T extends U ? T : never;
复制代码

Pick<Type, Keys>

Pick的定义很简单,就是从传入的Type中跳出对应的Keys属性,从而返回新的类型。

interface Props {
  name: string;
  label: number;
  value: boolean;
}

type ChildrenProps = Pick<Props, 'name' | 'label'>;
复制代码

Parameters<T>

Parameters<T>用于获得函数的参数类型组成的元组类型

源码中这样定义的

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

Omit<T, K>

3.5 版本之后,TypeScript 在 lib.es5.d.ts 里添加了一个 ​Omit<T, K>​ 帮助类型。​Omit<T, K>​ 类型让我们可以从另一个对象类型中剔除某些属性,并创建一个新的对象类型。

比如:

type User = {
    id: string;
    name: string;
    email: string;
};

type UserWithoutEmail = Omit<User, "email">;

// 等价于:

type UserWithoutEmail = {

    id: string;

    name: string;

};
复制代码

ReturnType

用法含义

Constructs a type consisting of the return type of function Type.

ResultType<type>接受传入一个函数类型为泛型,返回值为函数的返回类型。

type T0 = ReturnType<() => string>; // type T0 = string

type T1 = ReturnType<(s: string) => void>; // type T1 = void

复制代码

原理实现

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

ReturnType 源码中的类型定义中使用到了 extends 和 infer 关键字。

首先使用 extends 约束了传入的泛型类型 T 必须是一个函数,同时使用 infer 关键字推断函数的返回值,当然如果传入的泛型 T 不满足约束则会返回 any 。

Record<Keys,Type>

用法含义

构造一个新的对象类型,其属性键为Keys,属性值为Type。此实用程序可用于将一种类型的属性映射到另一种类型。

这是一个它的简单示例:

type keys = 'name' | 'title' | 'hello';

interface values {
  name: string;
  label?: number;
}

// Record内置类型可以将 传入的keys联合类型遍历作为key 
// 为每一个key的value赋值为 values从而形成一个全新的对象类型返回
const b: Record<keys, values> = {
  name: {
    name: 'wang',
    label: 1,
  },
  title: {
    name: 'hellp',
  },
  hello: {
    name: 'nihao',
  },
};

复制代码

同样我们常用 Record 类型在遍历上,比如:

// Record 常用遍历对象返回新的类型时使用
function mapping<K extends string | number | symbol, V, R>(
  obj: Record<K, V>,
  callback: (key: K, value: V) => R
): Record<K, R> {
  const result = {} as Record<K, R>;
  Object.keys(obj).forEach((key) => {
    const parseKey = key as K;
    const value = obj[key];
    result[key] = callback(parseKey, value);
  });
  return result;
}

mapping({ name: '19Qingfeng', company: 'Tencent' }, (key, value) => {
  return key + value;
});
复制代码

源码实现

看看它的源码本质上很简单,就是遍历传入的泛型 K 中每一个 key 值 P ,将每一个 P 作为 key 传入的 T 作为值类型重新组成一个对象类型。

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

React & TS内置类型

React.ReactNode

源码类型中关于ReactNode的类型定义

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
复制代码

可以看到ReactNode是一个类型别名,他是多种类型组成的联合类型。

其中ReactChildtype ReactChild = ReactElement | ReactText;

ReactPortal定义

 interface ReactPortal extends ReactElement {
        key: Key | null;
        children: ReactNode;
 }
复制代码

所以ReactNode是包含ReactElement类型的联合类型。换句话说ReactElement可以赋给ReactNode,但反过来不可以。

React.element

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
        type: T;
        props: P;
        key: Key | null;
    }
复制代码

可以看到React.Element类型接受传入两个泛型分别是jsx编译后的vdom对象的props组件本身

返回的仅仅是包含type,props,key的一个Object对象。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值