typescript
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
区别
TypeScript | JavaScript |
---|---|
JavaScript 的超集用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成 JavaScript 代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型或接口 |
支持 ES3,ES4,ES5 和 ES6 等 | 不支持编译其他 ES3,ES4,ES5 或 ES6 功能 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量文档和解决问题的支持 |
安装
1 | npm install -g typescript |
编译
1 | tsc helloworld.ts |
类型 (11种)
Boolean
1 | let isDone: boolean = false; |
Number
1 | let count: number = 10; |
String
1 | let name: string = "cosyer"; |
Array
1 | let list: number[] = [1, 2, 3]; |
Enum
使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。
数字枚举
1 | enum Direction { |
默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。上面的枚举示例代码经过编译后会生成以下代码:
1
2
3
4
5
6
7
8
9 ;
var Direction;
(function (Direction) {
Direction[(Direction["NORTH"] = 0)] = "NORTH";
Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
Direction[(Direction["EAST"] = 2)] = "EAST";
Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;1
2
3
4
5
6enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
字符串枚举
1 | enum Direction { |
编译生成:
1
2
3
4
5
6
7
8 ;
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
异构枚举
异构枚举的成员值是数字和字符串的混合:
1
2
3
4
5
6
7
8enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}1
2
3
4
5
6
7
8
9
10 ;
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 8] = "E";
Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));1
2console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A
Any
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。
1
2
3let notSure: any = 666;
notSure = "cosyer";
notSure = false;1
2
3
4
5
6
7let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
Unknown
就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使用示例:
1
2
3
4
5
6
7
8
9
10
11
12let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
对 value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?
1 | let value: unknown; |
unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。
将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。
Tuple
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:
1
2let tupleType: [string, boolean]; // 强制类型匹配
tupleType = ["cosyer", true];
Void
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:
1
2
3
4// 声明函数返回值为void
function warnUser(): void {
console.log("This is my warning message");
}
需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefined 或 null:
1
let unusable: void = undefined;
Null/Undefined
TypeScript 里,undefined 和 null 两者有各自的类型分别为 undefined 和 null。
1
2let u: undefined = undefined;
let n: null = null;
Never
never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
1 | // 返回never的函数必须存在无法达到的终点 |
在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
1
2
3
4
5
6
7
8
9
10
11
12type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:
type Foo = string | number | boolean;
复制代码然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保
controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
类型断言
尖括号
1 | let someValue: any = "this is a string"; |
as
1 | let someValue: any = "this is a string"; |
类型守卫
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:
in 关键字
1 | interface Admin { |
typeof 关键字
1 | function padLeft(value: string, padding: string | number) { |
typeof 类型保护只支持两种形式:typeof v === “typename” 和 typeof v !== typename,”typename” 必须是 “number”, “string”, “boolean” 或 “symbol”。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
instanceof 关键字
1 | interface Padder { |
自定义类型保护的类型谓词
1 | function isNumber(x: any): x is number { |
联合类型和类型别名
联合类型
联合类型通常与 null 或 undefined 一起使用:
1
2
3const sayHello = (name: string | undefined) => {
/* ... */
};1
2sayHello("cosyer");
sayHello(undefined);
可辨识联合
TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。
这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
可辨识
可辨识要求联合类型中的每个元素都含有一个单例类型属性(公共属性),比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}
interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}
联合类型
基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:
1
type Vehicle = Motorcycle | Car | Truck;
类型守卫
下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:
1
2
3
4
5
6
7const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);1
2Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:
1
2
3
4
5
6
7
8
9
10function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case "car":
return vehicle.transmission * EVALUATION_FACTOR;
case "truck":
return vehicle.capacity * EVALUATION_FACTOR;
case "motorcycle":
return vehicle.make * EVALUATION_FACTOR;
}
}
类型别名
类型别名用来给一个类型起个新名字(alias)。
1
2
3
4
5type Message = string | string[];
let greet = (message: Message) => {
// ...
};
交叉类型
TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
1 | interface IPerson { |
在上面示例中,我们首先为 IPerson 和 IWorker 类型定义了不同的成员,然后通过 & 运算符定义了 IStaff 交叉类型,所以该类型同时拥有 IPerson 和 IWorker 这两种类型的成员。
函数
区别
TypeScript | JavaScript |
---|---|
含有类型 | 无类型 |
箭头函数 | 箭头函数(ES2015) |
函数类型 | 无函数类型 |
必填和可选参数 | 所有参数都是可选的 |
默认参数 | 默认参数 |
剩余参数 | 剩余参数 |
函数重载 | 无函数重载 |
箭头函数
简单看下示例,详细可见JavaScript 中 this 的详解
1
2
3
4
5
6
7
8
9
10
11myBooks.forEach(() => console.log('reading'));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});
参数类型和返回类型
1 | function createUserId(name: string, id: number): string { |
函数类型
1 | let IdGenerator: (chars: string, nums: number) => string; |
可选参数和默认参数
1 | // 可选参数 |
在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。
剩余参数
1 | function push(array, ...items) { |
函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
1
2
3
4
5
6
7
8
9
10function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add("cosyer", " chenyu");
数组
数组解构
1 | let x: number; let y: number; let z: number; |
扩展运算符
1 | let two_array = [0, 1]; |
数组遍历
1 | let colors: string[] = ["red", "green", "blue"]; |
对象
对象解构
1 | let person = { |
对象展开扩展
1 | let person = { |
接口
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述
对象的形状(描述)
1 | interface Person { |
可选、只读属性
1 | interface Person { |
只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray
1 | let a: number[] = [1, 2, 3, 4]; |
泛型
软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
泛型接口
1 | interface GenericIdentityFn<T> { |
泛型类
1 | class GenericNumber<T> { |
泛型变量
对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:
- T(Type):表示一个 TypeScript 类型
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
泛型工具类型(6种)
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。
typeof
typeof 操作符可以用来获取一个变量声明或对象的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13interface Person {
name: string;
age: number;
}
const cosyer: Person = { name: 'cosyer', age: 25 };
type Cosyer= typeof cosyer; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
keyof
keyof 操作符可以用来一个对象中的所有 key 值:
1
2
3
4
5
6
7
8interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
in
in 用来遍历枚举类型:
1
2
3
4
5type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
1
2
3type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
1
2
3
4
5
6
7
8interface ILengthwise {
length: number;
}
function loggingIdentity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}1
2
3
4// error, number doesn't have a .length property
loggingIdentity(3)
loggingIdentity({length: 10, value: 3});
Partial
Partial1
2
3
4
5
6
7/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
1 | interface Todo { |
装饰器
装饰器是什么
- 它是一个表达式
- 该表达式被执行后,返回一个函数
- 函数的入参分别为 target、name 和 descriptor
- 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象
装饰器的分类
- 类装饰器(Class decorators)
- 属性装饰器(Property decorators)
- 方法装饰器(Method decorators)
- 参数装饰器(Parameter decorators)
类装饰器
1 | declare type ClassDecorator = <TFunction extends Function>( |
类装饰器顾名思义,就是用来装饰类的。它接收一个参数:
- target: TFunction - 被装饰的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello cosyer!");
};
}
@Greeter
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello cosyer!';
自定义参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}
@Greeter("Hello TS!")
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello TS!';
属性装饰器
1 | declare type PropertyDecorator = (target:Object, |
属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:
- target: Object - 被装饰的类
- propertyKey: string | symbol - 被装饰类的属性名
1 | function logProperty(target: any, key: string) { |
方法装饰器
1 | declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, |
方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:
- target: Object - 被装饰的类
- propertyKey: string | symbol - 方法名
- descriptor: TypePropertyDescript - 属性描述符
1 | function LogOutput(tarage: Function, key: string, descriptor: any) { |
参数装饰器
1 | declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, |
参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:
- target: Object - 被装饰的类
- propertyKey: string | symbol - 方法名
- parameterIndex: number - 方法中参数的索引值
1 | function Log(target: Function, key: string, parameterIndex: number) { |
类
类的属性和方法
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Greeter {
// 静态属性
static cname: string = "Greeter";
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return "Class name is Greeter";
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 ;
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
访问器(getter/setter)
在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "cosyer";
if (employee.fullName) {
console.log(employee.fullName);
}
继承
继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加自己新功能的能力,继承是类与类或者接口与接口之间最常见的关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
私有字段
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let cosyer = new Person("cosyer");
cosyer.#name;
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:
- 私有字段以 # 字符开头,有时我们称之为私有名称;
- 每个私有字段名称都唯一地限定于其包含的类;
- 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
- 私有字段不能在包含的类之外访问,甚至不能被检测到。
编译上下文
tsconfig.json 的作用
- 用于标识 TypeScript 项目的根路径;
- 用于配置 TypeScript 编译器;
- 用于指定编译的文件。
tsconfig.json 重要字段
- files - 设置要编译的文件的名称;
- include - 设置需要进行编译的文件,支持路径模式匹配;
- exclude - 设置无需进行编译的文件,支持路径模式匹配;
- compilerOptions - 设置与编译流程相关的选项。
compilerOptions 选项
1 | { |
仓库代码
相关问题
TypeScript 中,interface 和 type 的区别
问题记录
- Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index
1
2
3
4
5const formData = new FormData();
Object.keys(newCategory).map((k,i)=>{
var d =Object.values(newCategory)[i];
formData.append(k,d)
})tsconfig.json中配置suppressImplicitAnyIndexErrors为true