TypeScript 随想 · 实际应用与技巧

目录

  • 类型元编程
  • 内置工具类型窥探
  • 外部工具类型推荐
  • 新操作符
  • 声明文件

类型元编程

什么是元编程:

维基百科是这样描述的:元编程是一种编程技术,编写出来的计算机程序能够将其他程序作为数据来处理。意味着可以编写出这样的程序:它能够读取、生成、分析或者转换其它程序,甚至在运行时修改程序自身。在某些情况下,这使程序员可以最大限度地减少表达解决方案的代码行数,从而减少开发时间。它还允许程序更灵活有效地处理新情况而无需重新编译。

简单的说,元编程能够写出这样的代码:

  • 可以生成代码
  • 可以在运行时修改语言结构,这种现象被称为反射编程(Reflective Metaprogramming)或反射(Reflection)

什么是反射:

反射是元编程的一个分支,反射又有三个子分支:

  1. 自省(Introspection):代码能够自我检查、访问内部属性,我们可以据此获得代码的底层信息。
  2. 自我修改(Self-Modification):顾名思义,代码可以修改自身。
  3. 调解(Intercession):字面意思是「代他人行事」,在元编程中,调解的概念类似于包装(wrapping)、捕获(trapping)、拦截(intercepting)。

举个实际一点的例子

  • ES6(ECMAScript 2015)中用 Reflect(实现自省)和 Proxy(实现调解) 进行编码操作,称之为是一种元编程。
  • ES6 之前利用 eval 生成额外的代码,利用 Object.defineProperty 改变某个对象的语义等。

TypeScript 的类型元编程

个人感觉「元编程」这个概念并没有标准的明确的定义,所以本文这里就把在 TypeScript 中使用 infer、keyof、in 等关键字进行操作,称之为是 TypeScript 的类型元编程。或者说是「偏底层一点的特性」或者「骚操作」,大家明白其用途即可。

unknown

unknown type 是 TypeScript 中的 Top Type。符号是(⊤), 换句话说,就是任何类型都是 unknown 的子类型,unknown 是所有类型的父类型。换句最简单的话说,就是 任何值都可以赋值给类型是 unkown 的变量,与其对应的是,我们不能把一个 unkown 类型的值赋值给任意非 unkown 类型的值。

ts 复制代码
let a: unknown = undefined
a = Symbol('deep dark fantasy')
a = {}
a = false
a = '114514'
a = 1919n

let b : bigint = a; // Type 'unknown' is not assignable to type 'bigint'.

never

never 的行为与 unknown 相反,never 是 TypeScript 中的 Bottom Type,符号是(⊥),换句话说,就是任何类型都是 never 的父类型,never 是所有类型的子类型。

也可以顾名思义,就是「永远不会」=>「不要」的意思,never 与 infer 结合是常见体操姿势,下文会介绍。

ts 复制代码
let a: never = undefined // Type 'undefined' is not assignable to type 'never'

keyof

可以用于获取对象或数组等类型的所有键,并返回一个联合类型

ts 复制代码
interface Person {
  name: string
  age: number
}

type K1 = keyof Person  // "name" | "age"

type K2 = keyof []      // "length" | "toString" | "push" | "concat" | "join"

type K3 = keyof { [x: string]: Person }  // string | number

in

映射类型中,可以对联合类型进行遍历

ts 复制代码
type Keys = 'firstName' | 'lastName'

type Person = {
  [key in Keys]: string
}

// Person: { firstName: string; lastName: string; }

[]

索引操作符,使用 [] 操作符可以进行索引访问,所谓索引,就是根据一定的指向返回相应的值,比如数组的索引就是下标 0, 1, 2 等。TypeScript 里的索引签名有两种:字符串索引和数字索引。

字符串索引(对象)

对于纯对象类型,使用字符串索引,语法:T[key]

ts 复制代码
interface Person {
  name: string
  age: number
}

type Name = Person['name']  // Name: string

索引类型本身也是一种类型,因此还可以使用联合类型或者其他类型进行操作

ts 复制代码
type I1 = Person['name' | 'age']  // I1: string | number

type I2 = Person[keyof Person]    // I2: string | number

数字索引(数组)

对于类数组类型,使用数字索引,语法:T[number]

ts 复制代码
type MyArray = ['Alice', 'Bob', 'Eve']

type Alice = MyArray[0]       // 'Alice'

type Names = MyArray[number]  // 'Alice' | 'Bob' | 'Eve'

实际一点的例子

ts 复制代码
const PLAYS = [
  {
    value: 'DEFAULT',
    name: '支付送',
    desc: '用户支付后即获赠一张券',
  },
  {
    value: 'DELIVERY_FULL_AMOUNT',
    name: '满额送',
    desc: '用户支付满一定金额可获赠一张券',
    checkPermission: true,
    permissionName: 'fullAmount',
  },
]

type Play = typeof PLAYS[number]

/*
type Play = {
    value: string;
    name: string;
    desc: string;
    checkPermission?: undefined;
    permissionName?: undefined;
} | {
    value: string;
    name: string;
    desc: string;
    checkPermission: boolean;
    permissionName: string;
}
*/

泛型(generic)

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时非常有用。

实际例子,封装 ajax 请求库,支持不同的接口返回它该有的数据结构。

ts 复制代码
function ajax<T>(options: AjaxOptions): Promise<T> {
  // actual logic...
}

function queryAgencyRole() {
  return ajax<{ isAgencyRole: boolean }>({
    method: 'GET',
    url: '/activity/isAgencyRole.json',
  })
}

function queryActivityDetail() {
  return ajax<{ brandName: string; }>({
    method: 'GET',
    url: '/activity/activityDetail.json',
  })
}

const r1 = await queryAgencyRole()

r1.isAgencyRole  // r1 里可以拿到 isAgencyRole

const r2 = await queryActivityDetail()

r2.brandName     // r2 里可以拿到 brandName

extends

在官方的定义中称为条件类型(Conditional Types),可以理解为「三目运算」,T extends U ? X : Y,如果 T 是 U 的子集,那么就返回 X 否则就返回 Y。

  • 一般与泛型配合使用。
  • extends 会遍历联合类型,返回的也是联合类型。
ts 复制代码
type OnlyNumber<T> = T extends number ? T : never

type N = OnlyNumber<1 | 2 | true | 'a' | 'b'>  // 1 | 2

通常情况下,分布的联合类型是我们想要的, 但是也可以让 extends 不遍历联合类型,而作为一个整体进行判断与返回。只需要在 extends 关键字的左右两侧加上方括号 [] 进行修饰即可。

ts 复制代码
// 分布的条件类型
type ToArray<T> = T extends any ? T[] : never;

type R = ToArray<string | number>;

// type R = string[] | number[]
ini 复制代码
// 不分布的条件类型
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type R = ToArrayNonDist<string | number>;

// type R = (string | number)[]

infer

infer 关键字可以对运算过程中的类型进行存储,类似于定义一个变量。

内置的工具类型 ReturnType 就是基于此特性实现的。

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

type R1 = ReturnType<() => number>     // R1: number
type R2 = ReturnType<() => boolean[]>  // R2: boolean[]

递归(recursion)

在 TypeScript 中递归也是调用(或引用)自己,不过不一定需要跳出。

如下,定义 JSON 对象的标准类型结构。

ts 复制代码
// 定义基础类型集
type Primitive = string | number | boolean | null | undefined | bigint | symbol

// 定义 JSON 值
type JSONValue = Primitive | JSONObject | JSONArray

// 定义以纯对象开始的 JSON 类型
interface JSONObject {
  [key: string]: JSONValue
}

// 定义以数组开始的 JSON 类型
type JSONArray = Array<JSONValue>

提个小问题:为什么 TypeScript 不跳出递归也不会陷入死循环?

But apart from being computationally intensive, these types can hit an internal recursion depth limit on sufficiently-complex inputs. When that recursion limit is hit, that results in a compile-time error. In general, it's better not to use these types at all than to write something that fails on more realistic examples.

--from www.typescriptlang.or...

typeof

概念:像 TypeScript 这样的现代静态类型语言,一般具备两个放置语言实体的「空间」,即类型空间(type-level space)和值空间(value-level space),前者用于存放代码中的类型信息,在运行时会被完全擦除掉;后者用于存放代码中的「值」,会保留到运行时。

  • 值空间:变量、对象、数组、class、enum 等。
  • 类型空间:type、interface、class、enum 等。

typeof 的作用是把「值空间」的数据转换成「类型空间」的数据。

ts 复制代码
const MARKETING_TYPE = {
  ISV: 'ISV_FOR_MERCHANT',
  ISV_SELF: 'ISV_SELF',
  MERCHANT: 'MERCHANT_SELF',
}

type MarketingType = typeof MARKETING_TYPE

/*
type MarketingType = {
  ISV: string;
  ISV_SELF: string;
  MERCHANT: string;
}
*/

as const

as const 是一个类型断言,作用也是把「值空间」的数据转换成「类型空间」的数据,并且设置成只读。

ts 复制代码
let x = 'hello' as const;   // x: 'hello'

let y = [10, 20] as const;  // y: readonly [10, 20]

let z = { text: 'hello' } as const;  // z: { readonly text: 'hello' }

实际一点的例子:

ts 复制代码
const MARKETING_TYPE = {
  ISV: 'ISV_FOR_MERCHANT',
  ISV_SELF: 'ISV_SELF',
  MERCHANT: 'MERCHANT_SELF',
} as const

type MT = typeof MARKETING_TYPE

type MarketingType = MT[keyof MT]

/*
type MT = {
  readonly ISV: "ISV_FOR_MERCHANT";
  readonly ISV_SELF: "ISV_SELF";
  readonly MERCHANT: "MERCHANT_SELF";
}

type MarketingType = "ISV_FOR_MERCHANT" | "ISV_SELF" | "MERCHANT_SELF"
*/

内置工具类型窥探

TypeScript 内置了一些实用的工具类型,可以提高开发过程中类型转换的效率。

基于上面的了解,再来阅读内置工具类型就很轻松了,这里我们就列举几个常用或者有代表性的工具类型。

Partial

作用:把对象的每个属性都变成可选属性。

ts 复制代码
interface Todo {
  title: string;
  description: string;
}

type NewTodo = Partial<Todo>

/*
type NewTodo = {
  title?: string;
  description?: string;
}
*/

原理:把每个属性添加 ? 符号,使其变成可选属性。

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

Required

作用:与 Partial 相反,把对象的每个属性都变成必填属性。

ts 复制代码
interface Todo {
  title?: string;
  description?: string;
}

type NewTodo = Required<Todo>

/*
type NewTodo = {
  title: string;
  description: string;
}
*/

原理:给每个属性添加 -? 符号,- 指的是去除,-? 意思就是去除可选,就变成了 required 类型。

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

Readonly

作用:把对象的每个属性都变成只读属性。

ts 复制代码
interface Todo {
  title: string;
  description: string;
}

type NewTodo = Readonly<Todo>

/*
type NewTodo = {
  readonly title: string;
  readonly description: string;
}
*/

const todo: Readonly<Todo> = {
  title: 'Delete inactive users'
}

// Cannot assign to 'title' because it is a read-only property.
todo.title = "Hello";

原理:给每个属性添加 readonly 关键字,就变成了只读属性。

ts 复制代码
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Pick

作用:与 lodash 的 pick 方法一样,挑选对象里需要的键值返回新的对象,不过这里挑选的是类型。

ts 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 
type TodoPreview = Pick<Todo, 'title' | 'completed'>

/*
type TodoPreview = {
  title: string;
  completed: boolean;
}
*/

原理:使用条件类型约束传入的联合类型 K,然后再对符合条件的联合类型 K 进行遍历。

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

Omit

作用:与 Pick 工具方法相反,排除对象的某些键值。

ts 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 
type TodoPreview = Omit<Todo, 'description'>

/*
type TodoPreview = {
  title: string;
  completed: boolean;
}
*/

原理:与 Pick 类似,不过是先通过 Exclude 得到排除后的剩余属性,再遍历生成新对象类型。

scala 复制代码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Exclude

作用:排除联合类型里的一些成员类型。

ts 复制代码
type T0 = Exclude<'a' | 'b' | 'c', 'a'>        // T0: 'b' | 'c'

type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>  // T1: 'c'

原理:通过条件类型 extends 把不需要的类型排除掉。

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

Parameters

作用:获取函数的参数类型,返回的是一个元组类型

ts 复制代码
type T0 = Parameters<() => string>         // T0: []
type T1 = Parameters<(s: string) => void>  // T1: [s: string]

原理:通过 infer 关键字获取函数的参数类型并返回

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

ReturnType

作用:获取函数的返回类型

ts 复制代码
type R1 = ReturnType<() => number>      // R1: number
type R2 = ReturnType<() => boolean[]>   // R2: boolean[]

原理:通过 infer 关键字获取函数返回类型

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

Awaited

作用:取得无 Promise 包裹的原始类型。

ts 复制代码
type res = Promise<{ brandName: string }>

type R = Awaited<res>  // R: { brandName: string }

原理:如果是普通类型就返回该类型,如果是 Promise 类型,就用 infer 定义 then 的值,并返回。

ts 复制代码
type Awaited<T> =
  T extends null | undefined
    ? T
    : T extends object & { then(onfulfilled: infer F): any } // 检查 Promise 类型
      ? F extends (value: infer V, ...args: any) => any 
        ? Awaited<V>  // 递归 value 类型
        : never       // 不符合规则的 Promise 类型丢弃
      : T;    // 不是 Promise 类型直接返回

Promise 类型形状如下

ts 复制代码
/**
 * Represents the completion of an asynchronous operation
 */
interface Promise<T> {
    /**
     * Attaches callbacks for the resolution and/or rejection of the Promise.
     * @param onfulfilled The callback to execute when the Promise is resolved.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of which ever callback is executed.
     */
    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;

    /**
     * Attaches a callback for only the rejection of the Promise.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of the callback.
     */
    catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

获取 Promise 类型的另一种简单实现:

ts 复制代码
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T

外部工具类型推荐

市面上有 2 款 star 比较多的开源工具库

ValuesType

获取对象或数组的值类型。

ts 复制代码
interface Person {
  name: string
  age: number
}

const array = [0, 8, 3] as const

type R1 = ValuesType<Person>          // string | number
type R2 = ValuesType<typeof array>    // 0 | 8 | 3
type R3 = ValuesType<[8, 7, 6]>       // 8 | 7 | 6

实际例子:获取 JS 常量的值类型,避免重复劳动。

ts 复制代码
const MARKETING_TYPE = {
  ISV: 'ISV_FOR_MERCHANT',
  ISV_SELF: 'ISV_SELF',
  MERCHANT: 'MERCHANT_SELF',
} as const

type MarketingType = ValuesType<typeof MARKETING_TYPE>

// type MarketingType = "ISV_FOR_MERCHANT" | "ISV_SELF" | "MERCHANT_SELF"

实现原理:使用上文说到的「字符串索引」和「数字索引」来取值。

ts 复制代码
type ValuesType<
  T extends ReadonlyArray<any> | ArrayLike<any> | Record<any, any>
> = T extends ReadonlyArray<any>
  ? T[number]
  : T extends ArrayLike<any>
  ? T[number]
  : T extends object
  ? T[keyof T]
  : never;

新操作符

[2.0] Non-null assertion operator(非空断言符)

断言某个值存在

ts 复制代码
function createGoods(value: number): { type: string } | undefined {
  if (value < 5) {
    return
  }
  return { type: 'apple' }
}

const goods = createGoods(10)

goods.type  // ERROR: Object is possibly 'undefined'. (2532)

goods!.type  // ✅

[3.7] Optional Chaining(可选链操作符)

可选链操作符可以跳过值为 null 和 undefined 的情况,只在值存在的情况下才会执行后面的表达式。

ts 复制代码
let x = foo?.bar()

编译后的结果如下:

ts 复制代码
let x = foo === null || foo === void 0 ? void 0 : foo.bar();

实际场景的对比:

ts 复制代码
// before
if (user && user.address) {
  // ...
}

// after
if (user?.address) {
  // ...
}

// 语法:
obj.val?.prop     // 属性访问
obj.val?.[expr]   // 属性访问
obj.arr?.[index]  // 数组访问
obj.func?.(args)  // 函数调用

[3.7] Nullish Coalescing(双问号操作符)

ts 复制代码
// before
const isBlack = params.isBlack || true   // ❌
const isBlack = params.hasOwnProperty('isBlack') ? params.isBlack : true  // ✅

// after
const isBlack = params.isBlack ?? true  // ✅

[4.0] Short-Circuiting Assignment Operators(复合赋值操作符)

在 JavaScript 和许多程序语言中,称之为 Compound Assignment Operators(复合赋值操作符)

ts 复制代码
// Addition
// a = a + b
a += b;

// Subtraction
// a = a - b
a -= b;

// Multiplication
// a = a * b
a *= b;

// Division
// a = a / b
a /= b;

// Exponentiation
// a = a ** b
a **= b;

// Left Bit Shift
// a = a << b
a <<= b;

新增:

ts 复制代码
a &&= b   // a && (a = b)
a ||= b   // a || (a = b)
a ??= b   // a ?? (a = b)

示例:

ts 复制代码
let values: string[];

// Before
(values ?? (values = [])).push("hello");

// After
(values ??= []).push("hello");

声明文件

通常理解就是 .d.ts 文件,按功能可以分为:变量声明、模块声明、全局类型声明、三斜线指令等。

变量声明

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过

ts 复制代码
jQuery('#foo')  // ERROR: Cannot find name 'jQuery'.

TS 会报错,因为编译器不知道 $ 或 jQuery 是什么,所以需要声明这个全局变量让 TS 知道,通过 declare var 或 declare let/const 来声明它的类型。

ts 复制代码
// 声明变量 jQuery
declare var jQuery: (selector: string) => any;

// let 和 var 没有区别,更建议使用 let
declare let jQuery: (selector: string) => any;

// const 声明的变量不允许被修改
declare const jQuery: (selector: string) => any;

声明函数

ts 复制代码
// 声明函数
declare function greet(message: string): void;

// 使用
greet('hello')

声明类

ts 复制代码
// 声明类
declare class Animal {
  name: string;
  constructor(name: string);
  sayHi(): string;
}

// 使用
const piggy = new Animal('佩奇')
piggy.sayHi()

声明对象

ts 复制代码
// 声明对象
declare const jQuery: {
  version: string
  ajax: (url: string, settings?: any) => void
}

// 使用
console.log(jQuery.version)
jQuery.ajax('xxx')

还可以使用 namespace 命名空间来声明对象,早期 namespace 的出现是为了解决模块化而创造的关键字,随着 ES6 module 关键字的出现,为了避免功能混淆,现在建议不使用。

ts 复制代码
declare namespace jQuery {
  const version: string
  function ajax(url: string, settings?: any): void;
}

模块声明

通常我们引入 npm 包,它的声明文件可能来源于两个地方:

  • 包内置的类型文件,package.json 的 types 入口。
  • 安装 @types/xxx 对应的包类型文件。

假如上面两种方式都没有找到对应的声明文件,那么就需要手动为它写声明文件了,通过 declare module 来声明模块。

实例:手动修复 @alipay/h5data 的类型支持。

ts 复制代码
interface H5DataOption {
  env: 'dev' | 'test' | 'pre' | 'prod';
  autoCache: boolean;
}

declare module '@alipay/h5data' {
  export function fetchData<T extends any>(
    path: string,
    option?: Partial<H5DataOption>,
  ): Promise<T>;
}

// 使用
import { fetchData } from '@alipay/h5data'

const res = await fetchData<{ data: 'xxx' }>('url/xxx')

拓展模块类型

某些情况下,模块已经有类型声明文件了,但引入了一些插件,插件没有支持类型,这时就需要扩展模块的类型。还是通过 declare module 扩展,因为模块声明的类型会合并。

ts 复制代码
declare module 'moment' {
  export function foo(): string
}

// 使用
import moment from 'moment'
import 'moment-plugin'

moment.foo()

全局类型声明

类型的作用域

在 Typescript 中,只要文件存在 import 或 export 关键字,都被视为模块文件。也就是不管 .ts 文件还是 .d.ts 文件,如果存在上述关键字之一,则类型的作用域为当前文件;如果不存在上述关键字,文件内的变量、函数、枚举等类型都是以全局作用域存在于项目中的。

全局作用域声明全局类型

全局作用域内声明的类型皆为全局类型。

局部作用域声明全局类型

局部作用域内可以通过 declare global 声明全局类型。

ts 复制代码
import type { MarketingType } from '@/constants'

declare global {
  interface PageProps {
    layoutProps: {
      marketingType: MarketingType;
      isAgencyRole: boolean;
    };
  }
}

三斜线指令

三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。

三斜线指令的作用是为了描述模块之间的依赖关系,通常情况下并不会用到,不过在以下场景,还是比较有用。

  • 当在书写一个依赖其他类型的全局类型声明文件时
  • 当需要依赖一个全局变量的声明文件时
  • 当处理编译后 .d.ts 文件丢失的问题

当需要书写一个依赖其他类型的全局类型声明文件时

在全局变量的声明文件中,是不允许出现 import, export 关键字的,一旦出现了,那么当前的声明文件就不再是全局类型的声明文件了,所以这时就需要用到三斜线指令。

ts 复制代码
/// <reference types="jquery" />

declare function foo(options: JQuery.AjaxSettings): string;

依赖一个全局变量的声明文件

当需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,所以就需要使用三斜线指令来引入了。

ts 复制代码
/// <reference types="node" />

export function foo(p: NodeJS.Process): string;

当处理编译后 .d.ts 文件丢失的问题

在写项目的时候,项目里编写的 .d.ts 文件在 tsc 编译后,并不会放置到对应的 dist 目录下,这时候就需要手动指定依赖的全局类型。

ts 复制代码
/// <reference path="types/global.d.ts" />

// ValueOf 来自 global.d.ts
export declare type ComplexOptions = ValueOf<typeof complexOptions>;

reference

  • path: 指定类型文件的路径
  • types: 指定类型文件对应的包,例如 对应的类型文件是

参考

TypeScript Handbook:www.typescriptlang.or...

TypeScript Learning: github.com/Barrior/ty....

你不知道的 TypeScript 高级技巧:www.infoq.cn/article/...

TypeScript 入门教程:ts.xcatliu.com/basics...

读懂类型体操:TypeScript 类型元编程基础入门:zhuanlan.zhihu.com/p/...

JavaScript 元编程:chinese.freecodecamp....

其他资料

相关推荐
一介俗子6 小时前
TypeScript 中 extends 关键字
typescript
mez_Blog7 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
QGC二次开发10 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
2301_8010741512 小时前
TypeScript异常处理
前端·javascript·typescript
下雪天的夏风15 小时前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
天下无贼!1 天前
2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点
前端·javascript·vue.js·笔记·学习·typescript·html
Jorah3 天前
1. TypeScript基本语法
javascript·ubuntu·typescript
小白小白从不日白4 天前
TS axios封装
前端·typescript
aimmon5 天前
Superset二次开发之源码DependencyList.tsx 分析
前端·typescript·二次开发·bi·superset
下雪天的夏风5 天前
Vant 按需引入导致 Typescript,eslint 报错问题
前端·typescript·eslint