据官方描述:TypeScript 是 JavaScript 的超集,这意味着它可以完成 JavaScript 所做的所有事情,而且额外附带了一些能力。
JavaScript 本身是一种动态类型语言,这意味着变量可以改变类型。使用 TypeScript 的主要原因是就是为了给 JavaScript 添加静态类型。静态类型意味着变量的类型在程序中的任何时候都不能改变。它可以防止很多bug。
Typescript 值得学吗?
下面是学习 Typescript 的几个理由:
- 研究表明,TypeScript 可以发现 15% 的常见 bug。
- TypeScript可以让代码的可读性更好,你可以更好的理解代码是在做什么。
- TypeScript 可以为你申请到更多好工作。
- 学习 TypeScript可以使你对 JavaScript 有更好的理解和新的视角。
当然,使用Typescript也有一些缺点:
- TypeScript 的编写时间比 JavaScript 要长,因为你必须要指定类型,对于一些较小的独立项目,可能不值得使用。
- TypeScript 需要编译,项目越大消耗时间越长。
但是,相比提前发现更多的bug,花更长的时间也会值得的。
TypeScript 中的类型
原始类型
在Javascript中,有七种原始类型:
- string
- number
- bigint
- boolean
- undefined
- null
- symbol
原始类型都是不可变的,你可以为原始类型的变量重新分配一个新值,但不能像更改对象、数组和函数一样更改它的值。可以看下面的例子:
const name = 'ConardLi';
name.toLowerCase();
console.log(name);
let arr = [1, 3, 5];
arr.pop();
console.log(arr); //[1,3]
回到Typescript,我们可以在声明一个变量之后设置我们想要添加的类型:type
(我们一般称之为"类型注释"或"类型签名"):
let id: number = 5;
let firstname: string = 'ConardLi';
let hasDog: boolean = true;
let unit: number; // 声明变量而不赋值
unit = 5;
但是,如果变量有默认值的话,一般我们也不需要显式声明类型,Typescript会自动推断变量的类型(类型推断):
const id = 5; // number 类型
const firstname = 'ConardLi'; // string 类型
let hasDog = true; // boolean 类型
hasDog = 'yes'; // Type 'string' is not assignable to type 'boolean'
我们还可以将变量设置为联合类型(联合类型是可以分配多个类型的变量):
let age: string | number;
age = 17;
age = '17';
TypeScript 中的数组
在TypeScript中,你可以定义数组包含的数据类型:
const ids: number[] = [1, 2, 3, 4, 5]; // 只能包含 number
const names: string[] = ['ConardLi', 'Tom', 'Jerry']; // 只能包含 string
const options: boolean[] = [true, false, false]; //只能包含 true false
const books: object[] = [
{ name: 'Tom', animal: 'cat' },
{ name: 'Jerry', animal: 'mouse' },
]; // 只能包含对象
const arr: any[] = ['hello', 1, true]; // 啥都行,回到了 JS
ids.push(6);
ids.push('7'); // Argument of type 'string' is not assignable to parameter of type 'number'.
你也可以使用联合类型定义包含多种类型的数组:
const person: (string | number | boolean)[] = ['ConardLi', 1, true];
person[0] = 100;
person[1] = { name: 'ConardLi' }; //Type '{ name: string; }' is not assignable to type 'string | number | boolean'.
如果数组有默认值,Typescript同样也会进行类型推断:
onst person = ['ConardLi', 1, true]; // 和上面的例子一样
person[0] = 100;
person[1] = { name: 'ConardLi' }; //Type '{ name: string; }' is not assignable to type 'string | number | boolean'.
Typescript中可以定义一种特殊类型的数组:元组(Tuple)。元组是具有固定大小和已知数组类型的数组,它比常规数组更严格。
const person: [string, number, boolean] = ['ConardLi', 1, true];
person[0] = 17; //Type 'number' is not assignable to type 'string'.
Typescript中的对象
Typescript中的对象必须拥有所有正确的属性和值类型:
let person: {
name: string;
age: number;
isProgrammer: boolean;
};
person = {
name: 'ConardLi',
age: 17,
isProgrammer: true,
};
person.age = '17'; //Type 'string' is not assignable to type 'number'.
person = {
name: 'Chelsy',
age: 18,
}; //Property 'isProgrammer' is missing in type '{ name: string; age: number; }' but required in type '{ name: string; age: number; isProgrammer: boolean; }'.
在定义对象的类型时,我们通常会使用interface。如果我们需要检查多个对象是否具有相同的特定属性和值类型时,是很有用的:
interface Person {
name: string;
age: number;
isProgrammer: boolean;
}
let person1: Person = {
name: 'ConardLi',
age: 17,
isProgrammer: true,
};
let person2: Person = {
name: 'Tom',
age: 3,
isProgrammer: false,
};
我们还可以用函数的类型签名声明一个函数属性,通用函数和箭头函数都可以声明:
interface Animal {
eat(name: string): string;
speak: (name: string) => string;
}
const tom: Animal = {
eat: function (name: string) {
return `eat ${name}`;
},
speak: (name: String) => `speak ${name}`,
};
console.log(tom.eat("Jerry"));
console.log(tom.speak("hahaha"));
需要注意的是,虽然 eat、speak 分别是用普通函数和箭头函数声明的,但是它们具体是什么样的函数类型都可以,Typescript 是不关心这些的。
Typescript中的函数
我们可以定义函数参数和返回值的类型:
function circle(d: number): string {
return `圆的周长是:${Math.PI * d}`;
}
console.log(circle(10));
我们没必要明确声明circle是一个函数,Typescript会进行类型推断。Typescript还会推断函数的返回类型,但是如果函数体比较复杂,还会建议清晰的显式声明返回类型。
我们可以在参数后添加一个?,表示它为可选参数,另外参数的类型也可以是一个联合类型:
const add = (a: number, b: number, c?: number | string) => {
console.log(c);
return a + b;
};
console.log(add(5, 4, '可以是 number、string,也可以为空'));
如果函数没有返回值,在TS里表示为返回void,你也不需要显示声明,TS一样可以进行类型推断:
const log = (msg: string): void => {
console.log('打印一些内容: ' + msg);
};
any类型
使any类型,我们基本上可以将Typescript恢复为JavaScript:
let name: any = 'ConardLi';
name = 17;
name = { age: 17 };
如果代码中大量使用any,那Typescript也就失去了意义,所以我们应该尽量避免使用any。
DOM和类型转换
Typescript没办法像JavaScript那样访问DOM。这意味着每当我们尝试访问DOM元素时,Typescript都无法确定它们是否真的存在。
const link = document.querySelector("a");
console.log(link.href);//Object is possibly 'null'.
使用非空断言运算符(!),我们可以明确地告诉编译器一个表达式的值不是null和undefined。当编译器无法准确地进行类型推断时,这可能很有用:
const link = document.querySelector('a')!;
console.log(link.href);
这里我们没必要声明link变量的类型。这是因为Typescript可以通过类型推断确认它的类型为HTMLAnchorELement(超链接元素)。但是如果我们需要通过class或id来选择一个DOM元素呢?这时Typescript就没办法推断类型了:
const form = document.getElementById("signup-form");
console.log(form.method);
//Object is possibly 'null'.
//Property 'method' does not exist on type 'HTMLElement'.
我们需要告诉Typescript form确定是存在的,并且我们知道它的类型是HTMLFormElement。我们可以通过类型转换来做到这一点:
const form = document.getElementById('signup-form') as HTMLFormElement;
console.log(form.method); // post
TypeScript 还内置了一个 Event 对象。如果我们在表单中添加一个 submit 的事件侦听器,TypeScript 可以自动帮我们推断类型错误:
const form = document.getElementById('signup-form') as HTMLFormElement;
form.addEventListener('submit', (e: Event) => {
e.preventDefault(); // 阻止页面刷新
console.log(e.tarrget); // ERROR: Property 'tarrget' does not exist on type 'Event'. Did you mean 'target'?
});
Typescript中的类
我们可以定义类中每条数据的类型:
class Person {
name: string;
isCool: boolean;
age: number;
constructor(n: string, c: boolean, a: number) {
this.name = n;
this.isCool = c;
this.age = a;
}
sayHello() {
return `Hi,我是${this.name},我今年${this.age}岁了`;
}
}
const person1 = new Person("ConardLi", true, 17);
const person2 = new Person("Jerry", "yes", 20); //Argument of type 'string' is not assignable to parameter of type 'boolean'.
console.log(person1.sayHello());
我们可以创建一个仅包含从 Person 构造的对象数组:
let People: Person[] = [person1, person2];
我们可以给类的属性添加访问修饰符,Typescript还提供了一个新的readonly访问修饰符。
class Person {
readonly name: string;
private isCool: boolean; //类的私有属性、外部访问不到
protected email: string; //只能从这个类和子类中进行访问和修改
public age: number; //任何地方都可以访问和修改
constructor(n: string, c: boolean, e: string, a: number) {
this.name = n;
this.isCool = c;
this.email = e;
this.age = a;
}
sayHello() {
return `Hi,我是${this.name},我今年${this.age}岁了`;
}
}
const person1 = new Person("ConardLi", true, "conard@xx.com", 17);
// const person2 = new Person("Jerry", "yes", 20); //Argument of type 'string' is not assignable to parameter of type 'boolean'.
console.log(person1.name);
person1.name = "Jerry"; //Cannot assign to 'name' because it is a read-only property.
person1.isCool = false; //Property 'isCool' is private and only accessible within class 'Person'.
我们可以通过下面的写法,属性会在构造函数中自动分配,我们的二类会更加简洁:
class Person {
constructor(
readonly name: string,
private isCool: boolean,
protected email: string,
public age: number
) {}
}
如果我们省略访问修饰符,默认情况下属性都是 public,另外和 JavaScript 一样,类也是可以 extends 的。
TypeScript 中的接口
接口定义了对象的外观:
interface Person {
name: string;
age: number;
}
function sayHi(person: Person) {
console.log(`Hi ${person.name}`);
}
sayHi({
name: "ConardLi",
age: 17,
});
你还可以使用类型别名定义对象类型:
type Person = {
name: string;
age: number;
};
或者可以直接匿名定义对象类型:
function sayHi(person: { name: string; age: number }) {
console.log(`Hi ${person.name}`);
}
interface和type的相同与区别可以参考:https://editor.csdn.net/md/?articleId=123994867
一般来说,当你不知道用啥的时候,默认就用 interface 就行,直到 interface 满足不了我们的需求的时候再用 type。
类的interface
我们可以通过实现一个接口来告诉一个类它必须包含某些属性和方法:
interface HasFormatter {
format(): string;
}
class Person implements HasFormatter {
constructor(public username: string, protected password: string) {}
format() {
return this.username.toLocaleLowerCase();
}
}
let person1: HasFormatter;
let person2: HasFormatter;
person1 = new Person("ConardLi", "123456");
console.log(person1.format());
确保people是一个实现HasFormatter的对象数组(确保每个people都有format方法):
let people: HasFormatter[] = [];
people.push(person1);
people.push(person2);
泛型
泛型可以让我们创建一个可以在多种类型上工作的组件,它能够支持当前的数据类型,同时也能支持未来的数据类型,这大大提升了组件的可重用性。
const addID = (obj: object) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};
let person1 = addID({ name: "Chelsy", age: 18 });
console.log(person1.id);
console.log(person1.name); //Property 'name' does not exist on type '{ id: number; }'.
当我们尝试访问name属性时,Typescript会出错。这是因为当我们将一个对象传递给addID 时,我们并没有指定这个对象应该有什么属性,所以 TypeScript 不知道这个对象有什么属性。因此,TypeScript 知道的唯一属性返回对象的 id。
那么,我们怎么将任意对象传递给 addID,而且仍然可以告诉 TypeScript 该对象具有哪些属性和值?这种场景就可以使用泛型了, T 被称为类型参数:
// <T> 只是一种编写习惯 - 我们也可以用 <X> 或 <A>
const addID = <T>(obj: T) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};
let person1 = addID({ name: "Chelsy", age: 18 });
console.log(person1.id);
console.log(person1.name); //Chelsy
这是啥意思呢?现在当我们再将一个对象传递给 addID 时,我们已经告诉 TypeScript 来捕获它的类型了 —— 所以 T 就变成了我们传入的任何类型。addID 现在会知道我们传入的对象上有哪些属性。
但是,现在有一个问题:任何东西都可以传入addID,TypeScript 将捕获类型而且并不会报告问题:
let person1 = addID({ name: "ConardLi", age: 17 });
let person2 = addID("Jerry"); // 传递字符串也没问题
console.log(person1.id); // 188
console.log(person1.name); // ConardLi
console.log(person2.id);
console.log(person2.name); // Property 'name' does not exist on type '"Jerry" & { id: number; }'.
当我们传入一个字符串时,Typescript没有发现任何问题。只有我们尝试访问 name 属性时才会报告错误。所以,我们需要一个约束:我们需要通过将泛型类型 T 作为 object 的扩展,来告诉 TypeScript 只能接受对象:
const addID = <T extends object>(obj: T) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};
let person1 = addID({ name: "ConardLi", age: 17 });
let person2 = addID("Jerry"); // Argument of type 'string' is not assignable to parameter of type 'object'.
错误马上就被捕获了,但也不完全是。在 JavaScript 中,数组也是对象,所以我们仍然可以通过传入数组来逃避类型检查:
const addID = <T extends object>(obj: T) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};
let person2 = addID(["ConardLi", 17]);
console.log(person2.id); // 188
console.log(person2.name); // Property 'name' does not exist on type '(string | number)[] & { id: number; }'.
要解决这个问题,我们可以这样说:object 参数应该有一个带有字符串值的 name 属性:
interface O1 {
name: string;
}
const addID = <T extends O1>(obj: T) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};
let person2 = addID(["ConardLi", 17]);
//Argument of type '(string | number)[]' is not assignable to parameter of type 'O1'.
//Property 'name' is missing in type '(string | number)[]' but required in type 'O1'.
console.log(person2.id);
console.log(person2.name);
泛型允许在参数和返回类型提前未知的组件中具有类型安全。
在 TypeScript 中,泛型用于描述两个值之间的对应关系。在上面的例子中,返回类型与输入类型有关。我们用一个泛型来描述对应关系。
另一个例子:如果需要接受多个类型的函数,最好使用泛型而不是 any 。下面展示了使用 any 的问题:
function logLength(a: any) {
console.log(a.length); // No error
return a;
}
let hello = 'Hello world';
logLength(hello); // 11
let howMany = 8;
logLength(howMany); // undefined (but no TypeScript error - surely we want TypeScript to tell us we've tried to access a length property on a number!)
我们可以尝试使用泛型:
function logLength<T>(a: T) {
console.log(a.length); //Property 'length' does not exist on type 'T'.
return a;
}
好,至少我们现在得到了一些反馈,可以帮助我们持续改进我们的代码。
解决方案:使用一个泛型来扩展一个接口,确保传入的每个参数都有一个 length 属性:
interface hasLength {
length: number;
}
function logLength<T extends hasLength>(a: T) {
console.log(a.length);
return a;
}
let hello = "hello";
logLength(hello); //5
let howMany = 8;
logLength(howMany); //Argument of type 'number' is not assignable to parameter of type 'hasLength'.
我们也可以编写这样一个函数,它的参数是一个元素数组,这些元素都有一个length属性:
interface hasLength {
length: number;
}
function logLength<T extends hasLength>(a: T[]) {
a.forEach((item) => {
console.log(item.length);
});
}
let arr = [
"This string has a length prop",
["This", "arr", "has", "length"],
{ material: "plastic", length: 17 },
];
logLength(arr);
//29
//4
//17
泛型接口
当我们不知道对象中的某个值是什么类型时,可以使用泛型来传递该类型:
interface Person<T> {
name: string;
age: number;
documents: T;
}
const person1: Person<string[]> = {
name: "ConardLi",
age: 17,
documents: ["passport", "bank statement", "visa"],
};
const person2: Person<string> = {
name: "Tom",
age: 20,
documents: "passport, P45",
};
枚举
枚举是 TypeScript 给 JavaScript 带来的一个特殊特性。枚举允许我们定义或声明一组相关值,可以是数字或字符串,作为一组命名常量。
enum ResourceType {
BOOK,
AUTHOR,
FILM,
DIRECTOR,
PERSON,
}
console.log(ResourceType.BOOK); // 0
console.log(ResourceType.AUTHOR); // 1
// 从 1 开始
enum ResourceType {
BOOK = 1,
AUTHOR,
FILM,
DIRECTOR,
PERSON,
}
console.log(ResourceType.BOOK); // 1
console.log(ResourceType.AUTHOR); // 2
默认情况下,枚举是基于数字的 — 它们将字符串值存储为数字。但它们也可以是字符串:
enum Direction {
Up = 'Up',
Right = 'Right',
Down = 'Down',
Left = 'Left',
}
console.log(Direction.Right); // Right
console.log(Direction.Down); // Down
当我们有一组相关的常量时,枚举就可以派上用场了。例如,与在代码中使用非描述性数字不同,枚举通过描述性常量使代码更具可读性。
枚举还可以防止错误,因为当你输入枚举的名称时,智能提示将弹出可能选择的选项列表。
TypeScript 严格模式
建议在 tsconfig.json 中启用所有严格的类型检查操作文件。这可能会导致 TypeScript 报告更多的错误,但也更有助于帮你提前发现发现程序中更多的 bug。
// tsconfig.json
"strict": true
严格模式实际上就意味着:禁止隐式 any 和 严格的空检查。
禁止隐式any
在下面的函数中,TypeScript 已经推断出参数 a 是 any 类型的。当我们向该函数传递一个数字,并尝试打印一个 name 属性时,没有报错:
function logName(a) {
console.log(a.name);
}
logName(97); //undefined
打开 noImplicitAny 选项后,如果我们没有显式地声明 a 的类型,TypeScript 将立即标记一个错误:
function logName(a) {
//Parameter 'a' implicitly has an 'any' type.
console.log(a.name);
}
严格的空检查
当 strictNullChecks 选项为 false 时,TypeScript 实际上会忽略 null 和 undefined。这可能会在运行
时导致意外错误。
const getSong = () => {
return "song";
};
let whoSangThis: string = getSong();
const singles = [
{ song: "touch of grey", artist: "grateful dead" },
{ song: "paint it black", artist: "rolling stones" },
];
const single = singles.find((s) => s.song === whoSangThis);
console.log(single.artist);//Uncaught TypeError: Cannot read properties of undefined (reading 'artist')
当 strictNullChecks 设置为 true 时,null 和 undefined 有它们自己的类型,如果你将它们分配给一个期望具体值(例如,字符串)的变量,则会得到一个类型错误。
const getSong = () => {
return "song";
};
let whoSangThis: string = getSong();
const singles = [
{ song: "touch of grey", artist: "grateful dead" },
{ song: "paint it black", artist: "rolling stones" },
];
const single = singles.find((s) => s.song === whoSangThis);
console.log(single.artist); //Object is possibly 'undefined'.
TypeScript 基本上是告诉我们在使用 single 之前要确保它存在。我们需要先检查它是否为 null 或 undefined:
if (single) {
console.log(single.artist);
}
TypeScript 中的类型收窄
在 TypeScript 中,变量可以从不太精确的类型转移到更精确的类型,这个过程称为类型收窄。
下面是一个简单的例子,展示了当我们使用带有 typeof 的 if 语句时,TypeScript 如何将不太确定的 string | number 缩小到更特定的类型:
function addAnother(val: string | number) {
if (typeof val === 'string') {
// ts 将 val 视为一个字符串
return val.concat(' ' + val);
}
// ts 知道 val 在这里是一个数字
return val + val;
}
console.log(addAnother('哈哈')); // 哈哈 哈哈
console.log(addAnother(17)); // 34
另一个例子:下面,我们定义了一个名为 allVehicles 的联合类型,它可以是 Plane 或 Train 类型。
interface Vehicle {
topSpeed: number;
}
interface Train extends Vehicle {
carriages: number;
}
interface Plane extends Vehicle {
wingSpan: number;
}
type PlaneOrTrain = Plane | Train;
function getSpeedRatio(v: PlaneOrTrain) {
console.log(v.carriages);
//Property 'carriages' does not exist on type 'PlaneOrTrain'.
//Property 'carriages' does not exist on type 'Plane'.
}
由于 getSpeedRatio 函数处理了多种类型,我们需要一种方法来区分 v 是 Plane 还是 Train 。我们可以通过给这两种类型一个共同的区别属性来做到这一点,它带有一个字符串值:
interface Train extends Vehicle {
type: "Train";
carriages: number;
}
interface Plane extends Vehicle {
type: "Plane";
wingSpan: number;
}
type PlaneOrTrain = Plane | Train;
function getSpeedRatio(v: PlaneOrTrain) {
if (v.type === "Train") {
return v.topSpeed / v.carriages;
}
// 如果不是 Train,ts 知道它就是 Plane 了,聪明!
return v.topSpeed / v.wingSpan;
}
let bigTrain: Train = {
type: "Train",
topSpeed: 100,
carriages: 20,
};
console.log(getSpeedRatio(bigTrain)); // 5