为什么选择TypeScript
interface UserProps {
id: number; //用户id
name: string; //姓名
avatar?: string; //头像
}
function User(props: UserProps) {
...
}
环境搭建
安装TypeScript
npm i -g typescript
TypeScript 安装完成后,输入如下命令即可查看当前安装的 TypeScript 版本。
tsc -v
调试小技巧
以VS Code为例,在VS Code上面点击左下角叉号和感叹号图标或使用“Shift + Command + M”快捷键可以打开问题面板查看所有错误信息
因为 VS Code 中内置了最新版本的 TypeScript ,且这个内置的服务与手动安装的 TypeScript 完全隔离。因此,VS Code 支持在内置和手动安装版本之间动态切换语言服务,从而实现对不同版本的 TypeScript 的支持。
如果当前应用目录中安装了与内置服务不同版本的 TypeScript,我们就可以点击 VS Code 底部工具栏的版本号信息,从而实现 “use VS Code’s Version” 和 “use Workspace’s Version” 两者之间的随意切换。
工作区的版本是指项目根目录下node_modules/typescript里的TypeScript版本。如果两个版本之间存在不兼容的特性,就会造成开发阶段和构建阶段静态类型检测结论不一致的情况,因此,建议将 VS Code 语言服务配置成使用当前工作区的 TypeScript 版本。
TypeScript在线编辑器地址:
https://www.typescriptlang.org/zh/play?target=1&module=1&ts=4.3.2#code/Q
基础语法
类型注解的 TypeScript 与 JavaScript 完全一致
- let name = 'spencer';
只需要将变量后面添加: 类型注解就可以了,如下代码所示:
let name: string = 'spencer';
原始类型
JavaScript原始类型:string、number、bigint、boolean、undefined 和 symbol
注意事项:
? 虽然number和bigint都表示数字,但是这两个类型不兼容。
? TypeScript 还包含 Number、String、Boolean、Symbol 等类型(注意区分大小写),不要将它们和小写格式对应的 number、string、boolean、symbol 进行等价
let name: String = new String('spencer');
let name2: string = 'spencer';
console.log(name === name2); // false
数组类型
我们可以通过[]的方式定义数组类型,例如:
//子元素是数字类型的数组
let arrayAge: number[] = [28, 29, 30];
//子元素是字符串类型的数组
let arrayName: string[] = ['spencer', 'peter', 'john'];
也可以通过Array泛型的方式定义数组类型,例如:
//子元素是数字类型的数组
let arrayAge: Array<number> = [28, 29, 30];
//子元素是字符串类型的数组
let arrayName: Array<string> = ['spencer', 'peter', 'john'];
为了避免与JSX产生语法冲突,推荐使用[]的方式
特殊类型
1、any
any是指一个任意类型,用来选择性绕过静态类型检测。并且any 类型会在对象的调用链中进行传导,即所有 any 类型的任意属性的类型都是 any。
any是一个坏习惯,除非有充足的理由,否则应该尽量避免使用any
let anything: any = {};
anything.doAnything();
anything = 1;
anything = 'x';
let z = anything.x.y.z;
z();
2、unknown
unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量。与any不同的是,unknown更安全,我们可以将任意类型的值赋值给unknown,但是unknown类型的值只能赋值给unknown或any。
使用unknown时,TypeScript还是会对它做类型检测的,并且如果在使用过程中不缩小类型的话,在后续的执行过程中也是会出现错误的,例如:
let weight: unknown;
weight.toFixed(); // Object is of type 'unknown'.(2571)
应该进行类型缩小,才会避免报错,如下代码所示:
let weight: unknown;
if (typeof weight === 'number') {
weight.toFixed();
}
3、void
对于函数表示没有返回值的函数。
interface UserInfo = {
work: ()=>void
}
在严格模式下,对于变量设置void类型则是几乎没有什么用处,因为不能将void类型的变量赋值给除了any和unknown之外的任何类型变量。
4、undefined
undefined表示未定义的意思,在接口类型中有一定价值,例如:
interface UserInfo {
name: string;
age?: number;
}
因为age属性被标注为可缺省,就相当于它的类型是number类型与undefined的联合类型,但你不能手动将number | undefined 直接设置为age的类型,两者是不等价的,例如:
interface UserInfo {
name: string;
age: number | undefined;
}
上面?: 意味着可缺省,你可以不为这个属性赋值,但是类型undefined只是表示未定义,不代表该属性可缺省。
5、null
null表示值可能为空。它的主要价值在于接口的指定上,例如:
interface UserInfo: {
name: null | string
}
对于undefined和null我们在实际开发中要做好容错处理,例如:
const userInfo: {
name: null | string
}
if(userInfo.name != null){
...
}
6、never
never是指永远不会发生值的类型
例如一个抛错的函数,函数永远不会有返回值,所以返回值类型为never,代码如下所示:
function ThrowError(msg: string): never {
throw Error(msg);
}
never是所有类型的子类型,可以赋值给所有类型,但是反过来,除了never自身以外,其他类型都不能为never类型赋值。
在恒为false的条件判断下,变量类型就会被缩小为never类型,因为上面提到了never是所有类型的子类型,所以缩小到never类型,所以这种恒为false情况会提示错误给我们,如下代码所示:
const name: string = 'spencer';
if (typeof name === 'number') {
name.toFixed(); // Property 'toFixed' does not exist on type 'never'.ts(2339)
}
类型断言
const arrayAge: number[] = [22, 28, 30, 36];
const outAge: number = arrayAge.find((age:number) => age > 35);//ts报错
可以从上面的示例看出outAge 一定是一个数字(确切地讲是 36),因为 arrayAge 中明显有大于 35 的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript 看来,outAge 的类型既可能是数字,也可能是 undefined,所以ts会报错,此时我们不能把类型 undefined 分配给类型 number。
不过,我们可以使用一种笃定的方式——类型断言告诉 TypeScript 按照我们的方式做类型检查。
比如,我们可以使用 as 语法做类型断言,如下代码所示:
const arrayAge: number[] = [22, 28, 30, 36];
const outAge: number = arrayAge.find((age:number) => age > 35) as number;
非空断言
在值的后边添加 ! 断言操作符,它可以用来排除值为 null、undefined 的情况,例如:
let phoneNumber: null | undefined | number;
phoneNumber!.toString(); // ok
phoneNumber.toString(); // ts(2531)
只有在确保值一定不为空的时候再使用非空断言,如果在不能保证时候还是建议使用typeof这种类型条件判断的方式,例如:
let phoneNumber: null | undefined | number;
if (typeof phoneNumber === 'number') {
phoneNumber.toString(); // ok
}
字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型。字面量类型是集合类型的子类型,例如’this is string’是string类型的子类型。字面量类型的主要应用场景还是在于将多个字面量类型组合成一个联合类型,用来描述拥有明确成员的集合,例如:
type Language = 'js' | 'css';
function coding(language: Language) {
// ...
}
coding('js'); // ok
coding('java'); // ts(2345) Argument of type '"java"' is not assignable to parameter of type 'Language'
通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。
类类型
公共、私有、受保护的修饰符
? public 修饰的是在任何地方可见、公有的属性或方法;
? private 修饰的是仅在同一类中可见、私有的属性或方法;
? protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。
在不设置的时候默认都是public
class Person {
public firstName: string;
private lastName: string = 'Peter';
constructor(firstName: string) {
this.firstName = firstName;
this.lastName; // ok
}
}
const person = new Person('John');
console.log(person.firstName); // => "John"
son.firstName = 'Victor';
console.log(person.firstName); // => "Victor"
console.log(person.lastName); // Property 'lastName' is private and only accessible within class 'Person'.(2341)
TypeScript 中定义类的私有属性仅仅代表静态类型检测层面的私有。如果我们强制忽略 TypeScript 类型的检查错误,转译且运行 JavaScript 时依旧可以获取到私有属性,因为 JavaScript 并不支持真正意义上的私有属性。
受保护属性和方法示例如下:
class Person {
public firstName: string;
protected lastName: string = 'Peter';
constructor(firstName: string) {
this.firstName = firstName;
this.lastName; // ok
}
}
class Programmer extends Person {
constructor(firstName: string) {
super(firstName);
}
public getLastName() {
return this.lastName;
}
}
const programmer = new Programmer('John');
console.log(programmer.getLastName()); // => "Peter"
programmer.lastName; //Property 'lastName' is protected and only accessible within class 'Person' and its subclasses.(2445)
只读修饰符 - readonly
class Person {
public readonly firstName: string;
constructor(firstName: string) {
this.firstName = firstName;
}
}
const person = new Person('John');
person.firstName = 'Victor'; // ts(2540) Cannot assign to 'firstName' because it is a read-only property.
静态属性
静态属性可以直接通过类访问,而不用实例化
class Person {
static name = 'Spencer';
static getAge() {
//...
}
}
console.log(Person.name); // => "Spencer"
console.log(Person.getAge());
抽象类
它是一种不能被实例化仅能被子类继承的特殊类。我们可以使用抽象类定义派生类需要实现的属性和方法如下代码所示:
JavaScript
abstract class Adder {
abstract x: number;
abstract y: number;
abstract add(): number;
displayName = 'Adder';
addTwice(): number {
return (this.x + this.y) * 2;
}
}
class NumAdder extends Adder {
x: number;
y: number;
constructor(x: number, y: number) {
super();
this.x = x;
this.y = y;
}
add(): number {
return this.x + this.y;
}
}
const numAdder = new NumAdder(1, 2);
console.log(numAdder.displayName); // => "Adder"
console.log(numAdder.add()); // => 3
console.log(numAdder.addTwice()); // => 6
继承自Adder的派生类 NumAdder, 实现了抽象类里定义的 x、y 抽象属性和 add 抽象方法。如果派生类中缺少对 x、y、add 这三者中任意一个抽象成员的实现,ts是会报错提示的
接口类型
function work(action: { coding: string }) {
console.log(action.coding);
}
let myAction = { meeting: "have a meeting", coding: "TypeScript" };
work(myAction);
换成接口类型写法
使用interface关键字来抽离可复用的接口类型
interface ActionValue {
coding: string;
}
function work(action: ActionValue) {
console.log(action.coding);
}
let myAction = { meeting: "have a meeting", coding: "TypeScript" };
work(myAction);
let myAction2: ActionValue = { meeting: "have a meeting", coding: "CSS" };
work(myAction2);//报错
索引签名
在实际工作中,使用接口类型比较多的时候是对象,对比React组件的props或state等等。这个时候我们一般用索引签名来定义对象映射结构,通过[索引名: 类型]的方式约束索引的类型。
索引名称类型分为string和number两种,例如:
interface LanguageRankInterface {
[rank: number]: string;
}
interface LanguageYearInterface {
[name: string]: number;
}
let LanguageRankMap: LanguageRankInterface = {
1: 'TypeScript', // ok
2: 'JavaScript', // ok
'index': '2021' // ts(2322) 不存在的属性名
};
let LanguageMap: LanguageYearInterface = {
'TypeScript': 2012, // ok
'JavaScript': 1995, // ok
1: 1970 // ok
};
在上述示例中,数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 1 或 ‘1’ 索引对象时,这两者等价。
Type类型别名
通过type 别名名称 = 类型定义的形式来定义类型别名,如下所示:
type User = {
name: string;
age: number;
}
Interface 与 Type 的区别
大多数情况下都可以互相替代,但是如果遇到重复定义的时候两者会有区别,Interface重复定义接口类型,他的属性会叠加,Type重复定义类型别名,ts会报错。
{
interface Language {
id: number;
}
interface Language {
name: string;
}
let lang: Language = {
id: 1, // ok
name: 'name' // ok
}
}
{
/** ts(2300) 重复的标志 */
type Language = {
id: number;
}
/** ts(2300) 重复的标志 */
type Language = {
name: string;
}
let lang: Language = {
id: 1,
name: 'name'
}
}
泛型
泛型指的是类型参数化,即将原来某种具体的类型进行参数化。和定义函数参数一样,我们可以给泛型定义若干个类型参数,并在调用时给泛型传入明确的类型参数。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
比如定义了一个 reflect 函数 ,它可以接收一个任意类型的参数,并原封不动地返回参数的值和类型,那我们该如何描述这个函数呢?好像只能用any了。
function reflect(param: any) {
return param;
}
const str = reflect('string');
const num = reflect(1);
此时,泛型正好可以满足这样的诉求,因为泛型就是将参数的类型定义为一个参数、变量,而不是一个明确的类型,等到函数调用时再传入明确的类型。
我们可以通过尖括号 <> 语法给函数定义一个泛型参数 P,并指定 param 参数的类型为 P ,如下代码所示:
function reflect<P>(param: P) {
return param;
}
const reflectStr = reflect<string>('string');
const reflectNum = reflect<number>(1);
这里我们可以看到,尖括号中的 P 表示泛型参数的定义,param 后的 P 表示参数的类型是泛型 P(即类型受 P 约束)。
然后在调用函数时,我们也通过 <> 语法指定了如下所示的 string、number 类型入参,相应地,reflectStr 的类型是 string,reflectNum 的类型是 number。
另外,如果调用泛型函数时受泛型约束的参数有传值,泛型参数的入参可以从参数的类型中进行推断,而无须再显式指定类型(可缺省),因此上边的示例可以简写为如下示例:
const reflectStr2 = reflect('string');
const reflectNum2 = reflect(1);
注意:函数的泛型入参必须和参数/参数成员建立有效的约束关系才有实际意义。
联合类型
联合类型:用来表示变量、参数的类型不是单一原子类型,而可能是多种不同的类型的组合。
通过“|”操作符分隔类型的语法来表示联合类型,例如:
function getPhoneNum(val: number | string) {
// ...
}
我们也可以把接口类型联合起来表示更复杂的结构,如下所示示例:
interface FE {
coding(): void;
meeting(): void;
}
interface PM {
writePrd(): void;
meeting(): void;
}
const getEmployee: () => FE | PM = () => {
return {
// ...
} as FE | PM;
};
const Employee = getEmployee();
Employee.meeting(); // ok
Employee.coding(); // ts(2339) 'PM' 没有 'coding' 属性; 'FE | PM' 没有 'coding' 属性
从上边的示例可以看到,在联合类型中,我们可以直接访问各个接口成员都拥有的属性、方法,且不会提示类型错误。但是,如果是个别成员特有的属性、方法,我们就需要区分对待了。
交叉类型
交叉类型:把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性。
使用“&”操作符来声明交叉类型,如下代码所示:
type Useless = string & number;
很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Useless 的类型就是个 never。
合并接口类型
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:
type NewType = { id: number; name: string; } & { age: number };
const mixed: NewType = {
id: 1,
name: 'name',
age: 18
}
在上述示例中,我们通过交叉类型,使得 NewType 同时拥有了 id、name、age 三个属性,这里我们可以试着将合并接口类型理解为求并集。
如果合并的多个接口类型存在同名属性会是什么效果呢?
要看同名属性的类型是否兼容,如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 name 属性类型一个是 number,另一个是 string,合并后,name 属性的类型就是 number 和 string 两个原子类型的交叉类型,即 never,如下代码所示:
type NewType = { id: number; name: string; }
& { age: number; name: number; };
const mixed: NewType = {
id: 1,
name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
age: 2
};
此时,我们赋予 mixed 任意类型的 name 属性值都会提示类型错误。而如果我们不设置 name 属性,又会提示一个缺少必选的 name 属性的错误。在这种情况下,就意味着上述代码中交叉出来的 NewType 类型是一个无用类型。
如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 name 属性的类型就是两者中的子类型。
如下所示示例中 name 属性的类型就是数字字面量类型 2,因此,我们不能把任何非 2 之外的值赋予 name 属性。
type NewType = { id: number; name: 2; } & { age: number; name: number; };
let mixed: NewType = {
id: 1,
name: 2, // ok
age: 2
};
mixed = {
id: 1,
name: 22, // '22' 类型不能赋给 '2' 类型
age: 2
};
合并联合类型
合并联合类型为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员。可以将合并联合类型理解为求交集。
type UnionA = 'px' | 'em' | 'rem' | '%';
type UnionB = 'vh' | 'em' | 'rem' | 'pt';
type IntersectionUnion = UnionA & UnionB;
const intersectionA: IntersectionUnion = 'em'; // ok
const intersectionB: IntersectionUnion = 'rem'; // ok
const intersectionC: IntersectionUnion = 'px'; // ts(2322)
const intersectionD: IntersectionUnion = 'pt'; // ts(2322)
如果多个联合类型中没有相同的类型成员,交叉出来的类型就是 never 类型了
类型缩减
如果联合类型中类型都是父子关系,则会产生类型缩减,只会保留原始类型、枚举类型等父类型,例如:
type URStr = 'string' | string; // 类型是 string
type URNum = 2 | number; // 类型是 number
type URBoolen = true | boolean; // 类型是 boolean