TypeScript 面试常用一句话原理

120 阅读7分钟

juejin.cn/post/723631…

关于如何解释泛型,我看到的最好的一句话概括把明确类型的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型,简单点来讲我们可以将泛型理解成为把类型当作参数一样去传递。

在 TypeScript 中,你可以在函数、类、接口等地方使用泛型。以下是一些泛型的示例:

TS泛型简单来说就是类型参数,在定义某些函数、接口和类时,不写死类型,而是改用类型参数的形式,让类型更加灵活。

举个简单的例子,我们定义一个数据响应体的接口:

interface IResponseData<T>{
    code: number;
    message?: string;
    data: T;
}

其中data的类型并没有写死,而是可以在我们使用的时候传入:

interface User {
  id: number;
  name: string;
  email: string;
}

// 使用时传入User类型
const response: IResponseData<User> = {
  code: 200,
  message: "Success",
  data: {
    id: 1,
    name: "xiaoming",
    email: "xxx@qq.com"
  }
};

泛型哪些场景使用

泛型能够增强代码的可读性、可维护性和灵活性,使得你能够更好地编写类型安全且高效的代码。

import axios, { AxiosResponse } from 'axios';

async function fetchData<T>(url: string): Promise<T> {
    const response: AxiosResponse<T> = await axios.get(url);
    return response.data;
}

// 使用泛型请求用户信息
const user = await fetchData<UserInfo>('/api/user');
// 使用泛型请求聊天记录
const messages = await fetchData<Message[]>('/api/messages');

8. TS里怎么处理第三方库类型,怎么给第三方库编写类型文件?

TS社区维护了一个名为DefinitelyTyped的项目,提供了大量的第三方库的类型定义文件,大多数三方库类型文件都可以直接在这里面下载。

但是如果第三方库没有提供类型定义文件时,我们可以通过手动编写类型文件的方式,为第三方库添加类型支持。

给三方库编写类型的文件步骤如下:

  1. 创建d.ts文件:在项目中创建一个新的d.ts文件,文件名可以与库名相同,例如lodash.d.ts。

  2. 定义模块:使用declare module语句定义模块名,模块名应与库的导出模块名一致。例如,对于lodash库,可以这样定义模块:

keyof 关键字

所谓 keyof 关键字代表它接受一个对象类型作为参数,并返回该对象所有 key 值组成的联合类型。

比如:

interface IProps {
  name: string;
  age: number;
  sex: string;
}

// Keys 类型为 'name' | 'age' | 'sex' 组成的联合类型
type Keys = keyof IProps

is 关键字

原本是不打算讲述这个基础概念的,奈何之前在一次面试中因为 is 关键字翻了车哈哈。 所谓 is 关键字其实更多用在函数的返回值上,用来表示对于函数返回值的类型保护。

function isString(value: any): value is string {
    return typeof value === 'string';
}

function example(input: any) {
    if (isString(input)) {
        // 在这个代码块中,TypeScript 将 input 视为 string 类型
        console.log(input.length);
    } else {
        // 在这个代码块中,TypeScript 将 input 视为除了 string 之外的其他类型
        console.log("Not a string");
    }
}

infer 关键字

infer 是 TypeScript 中的一个关键字,主要用于条件类型(Conditional Types)中,它的作用是引入一个新的类型变量,并将该变量用于推断某个类型的一部分。infer 通常与 extends 关键字一起使用。

在条件类型中,infer 允许我们在条件判断中推断出类型。这对于从函数返回值或表达式中提取类型信息非常有用。

type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function exampleFunction(): string {
    return "Hello, TypeScript!";
}

type ReturnTypeOfExample = ExtractReturnType<typeof exampleFunction>;
// ReturnTypeOfExample 的类型为 string

unknown & any

在 TypeScript 中同样存在一个高级类型 unknown ,它可以代表任意类型的值,这一点和 any 是非常类型的。

但是我们清楚将类型声明为 any 之后会跳过任何类型检查,比如这样:

let myName: any;

 
myName = 1

// 这明显是一个bug

myName()

而 unknown 和 any 代表的含义完全是不一样的,虽然 unknown 可以和 any 一样代表任意类型的值,但是这并不代表它可以绕过 TS 的类型检查

而 unknown 和 any 代表的含义完全是不一样的,虽然 unknown 可以和 any 一样代表任意类型的值,但是这并不代表它可以绕过 TS 的类型检查。

let myName: unknown;

myName = 1

// ts error: unknown 无法被调用,这被认为是不安全的
myName()

// 使用typeof保护myName类型为function
if (typeof myName === 'function') {
  // 此时myName的类型从unknown变为function
  // 可以正常调用
  myName()
}

通俗来说 unknown 就代表一些并不会绕过类型检查但又暂时无法确定值的类型,我们在一些无法确定函数参数(返回值)类型中 unknown 使用的场景非常多。比如:

// 在不确定函数参数的类型时
// 将函数的参数声明为unknown类型而非any
// TS同样会对于unknown进行类型检测,而any就不会
function resultValueBySome(val:unknown) {
  if (typeof val === 'string') {
    // 此时 val 是string类型
    // do someThing
  } else if (typeof val === 'number') {
    // 此时 val 是number类型
    // do someThing
  }
  // ...
}

unknown类型可以接收任意类型的值,但并不支持将unknown赋值给其他类型。

any类型同样支持接收任意类型的值,同时赋值给其他任意类型(除了never)。

typscript模块(esmodule)与命名空间(namesapce)

相同点

都可以作为模块来使用。划分作用域,不同模块(不同作用域)的相同相同变量不冲突。
不使用模块的情况
没有使用模块,全局作用域下存在同名变量,出现命名冲突

使用esmodule
只要使用了import/export 文件就会变成一个模块,全局作用域下没有同名变量,不会出现命名冲突。
在这里插入图片描述
使用namespace
全局作用域下没有同名变量,不会出现命名冲突
在这里插入图片描述

不同点

  • 前者一个文件就是一个模块;后者一个文件可以有多个namespace(模块),不同的文件可以有同名的namespace(模块)。
  • 查找方式不同:esmodule按路径查找,而namespace直接在全局下找。namespace始终编译到全局。 namespace暴露在全局

在这里插入图片描述
esmodule按路径查找
在这里插入图片描述
注意不要将namespace和esmodule混用,不然获取全局下的namespace了

namespace内部实现方式

namespace A{
    function A() {
  
}
}
// 内部实现
var A;
(function (A_1) {
    function A() {
    }
})(A || (A = {}));

可以看到namespace 本身就是IIFE,还是闭包函数。namespace采用的是最古老的模块方案。

3.类型别名type和接口interface有什么区别?

  1. 类型别名不能被继承或者实现,接口可以被继承或者实现。
  2. 类型别名可以定义任何类型,包括联合类型、交叉类型、字面量类型、原始类型等。接口只能定义对象类型,包括属性、方法、索引等。
  3. 类型别名通常用于为复杂类型创建别名,以方便在代码中使用。接口通常用于定义某个实体的结构,以及实现该结构的对象或类。

你知道哪些工具类型,怎么用?

工具类型主要用于处理和转换已有类型,它们不是实际的类型,而是用来处理类型的工具。

工具类型可以认为是TS类型的工具函数,把原有类型当参数来处理。

常用工具类型有:

  1. Partial<T>:将类型 T 的所有属性变为可选属性。
  2. Required<T>:将类型 T 的所有属性变为必选属性。
  3. Readonly<T>:将类型 T 的所有属性变为只读属性。
  4. Record<K, T>:创建一个类型,其中属性名为类型 K 中的值,属性值为类型 T 中的值。
  5. Pick<T, K>:从类型 T 中选择属性名为类型 K 中的属性,创建一个新类型。
  6. Omit<T, K>:从类型 T 中排除属性名为类型 K 中的属性,创建一个新类型。
  7. Exclude<T, U>:从类型 T 中排除类型 U 中的所有属性。
  8. Extract<T, U>:从类型 T 中提取类型 U 中存在的所有属性。
  9. NonNullable<T>:从类型 T 中移除 null 和 undefined。
  10. ReturnType<T>:获取函数类型 T 的返回值类型。
declare module 'lodash' {
  // 在此处添加类型定义
}

最后:模块优于命名空间,最好不要使用命名空间。理由1:namespace是全局的立即执行函数。理由2:namespace内部使用了闭包… 光这两点就没有理由使用它。