TypeScript是一种由Microsoft开发的编程语言,它是JavaScript的超集,通过添加静态类型系统来增强JavaScript的功能。在这篇博客中,我们将简单的了解一下TypeScript的一些基础知识点。
首先我们来看一下 TypeScript 有哪些优势吧
- 类型就是文档,而且这份文档编译器和人都能读懂,比注释管用多了。有了类型可以在编译期检查错误,IDE可以给你智能提示提高工作利率
- 强大的IDE支持
- 如今前端项目越来越庞大,越来越复杂,静态类型简直就是刚需
- 我们在进行代码重构的时候,可以很快发现一些接口或者参数不兼容的情况,提前暴露问题
- 在调用接口的时候就可以很明确的知道应该传递哪些参数,返回值是什么类型。
TypeScript 缺点
了解完优势我们也要看一下它有那些缺点
- 有一定的学习成本,需要理解
接口(Interfaces)
、泛型(Generics)
、类(Classes)
、枚举类型(Enums)
等前端工程师可能不是很熟悉的概念 - 短期项目开发
可能会增加一些开发成本,毕竟要写很多类型的定义 - 集成到构建流程需要一些工作量
- 可能和一些库结合的还不是很完美
TypeScript 的类型检查
类型推断:
- 基础类型推断
- 最佳通用类型推断
- 上下文类型推断
类型兼容:
- 结构之间兼容:成员少的兼容成员多的
- 函数之间兼容:参数多的兼容参数少的
类型保护:
- 类型保护允许你使用更小范围下的对象类型
泛型
泛型(Generics
)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T
和 U
,而不需要开发人员显式指定它们。
联合类型
由于 JavaScript
是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
由于 TypeScript
是 JavaScript
的超集,因此以上的代码可以直接在 TypeScript
中使用,但当 TypeScript
编译器开启 noImplicitAny
的配置项时,以上代码会提示以下错误信息:
Parameter 'x' implicitly has an 'any' type. Parameter 'y' implicitly has an 'any' type.
该信息告诉我们参数 x 和参数 y 隐式具有 any
类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add
函数同时支持 string
和 number
类型,因此我们可以定义一个 string
|
number
联合类型,同时我们为该联合类型取个别名:
type Combinable = string | number;
在定义完 Combinable
联合类型后,我们来更新一下 add
函数:
function add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
为 add
函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add
函数就完美了么,我们来实际测试一下:
const result = add("semlinker", " kakuqo"); result.split(" ");
在上面代码中,我们分别使用 ‘semlinker
’ 和 ’ kakuqo
’ 这两个字符串作为参数调用 add
函数,并把调用结果保存到一个名为 result
的变量上,这时候我们想当然的认为此时 result
的变量的类型为 string
,所以我们就可以正常调用字符串对象上的 split
方法。但这时 TypeScript
编译器又出现以下错误信息了:
Property 'split' does not exist on type 'Combinable'.
Property 'split' does not exist on type 'number'.
很明显 Combinable
和 number
类型的对象上并不存在 split
属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript
提供的函数重载。
Type 和 Interface 的区别 (敲黑板,面试时的高频率问题)
interface
可以重复声明,type
定义后不能重复声明interface
可以通过extends
关键字来继承接口。而type
通过&
来实现类似于继承的功能interface
能够声明合并,type
可以声明基本类型别名,联合类型,元组等类型,严谨一点讲interface
是定义,type
是声明
看一下下面的例子能帮助你更好的理解
interface User {
name: string
age: number
}
interface User {
sex: string
}
/*
User 接口为 {
name: string
age: number
sex: string
}
*/
// type 可以而 interface 不行
// type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string
// 联合类型
interface Dog {
wang();
}
interface Cat {
miao();
}
type Pet = Dog | Cat
// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
// type 语句中还可以使用 typeof 获取实例的 类型进行赋值
// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div
// 其他的骚操作
type StringOrNumber = string | number;
type Text = string | {
text: string };
type NameLookup = Dictionary < string, Person>;
type Callback = (data: T) => void;
type Pair = [T, T];
type Coordinates = Pair; type Tree = T | {
left: Tree, right: Tree };
Objects/Functions
interface Point {
x: number;
y: number;
}
interface setPoint {
(x: number, y: number): void;
}
type Point = {
x: number;
y: number;
}; type setPoint = (x: number, y: number) => void;
类型别名
- 与接口类型不一样,类型别名可以用于一些其他类型,比如
原始类型
、联合类型
、元组
// primitive
type Name = string;
// object
type PartialPointX = {
x: number };
type PartialPointY = {
y: number };
// union
type PartialPoint = PartialPointX | PartialPointY;
//tuple
type Data = [number, string];
extends 关键字
接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的。
//interface extends interface
interface PartialPointX {
x: number;
}
interface Point extends PartialPointX {
y: number;
}
//type alias extends type alias
type PartialPointX = {
x: number };
type Point = PartialPointX & {
y: number };
//interface extends type alias
type PartialPointX = {
x: number };
interface Point extends PartialPointX {
y: number;
}
//type alias extends interface
interface PartialPointX {
x: number;
}
type Point = PartialPointX & {
y: number };
implements 关键字
一般情况下,一个类只能继承自另一个类,但是不同类之间可以有一些共同特性,这时候就可以使用 implements
关键字实现将这些共同特性提取成一个接口(interface
);
类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x: 1;
y: 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = {
x: number } | {
y: number };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint {
// Error
x = 1;
y = 2;
}
声明合并 Declaration merging
声明合并(Declaration Merging
) 是 Typescript
的一个高级特性,顾名思义,声明合并就是将相同名称的一个或多个声明合并为单个定义。它最初的设计目的是为了解决早期 JavaScript
模块化开发中的类型定义问题,有兴趣的可以去了解一下。
与类型别名不同,接口可以定义多次,会被自动合并为单个接口。
interface Point {
x: number;
}
interface Point {
y: number;
}
const point: Point = {
x: 1, y: 2 };
TypeScript 中的高级类型
简单举几个类型和范例给大家看看
Partial
Partial
是 TypeScript
中的一个工具类型,它的作用是将传入的属性变为可选项。
keyof
可以用来取得一个对象接口的所有key
值 产生联合类型
in
可以遍历枚举类型,所以经常一起使用
type Partial<T> = {
[ P in keyof T]?: T[P]}
// Partial<T> 接受一个泛型参数 T,并返回一个新的类型,新类型与 T 相同,但是 T 类型中的所有属性都变为可选属性
Required
它的作用是使 T 中的所有属性都变成必需的
type Required<T> = {
[P in keyof T]-?:T[P]}
这里有一个有意思的用法 -?
, 这里很好理解就是将可选项代表的 ?
去掉, 从而让这个类型变成必选项。与之对应的还有个+?
, 这个含义自然与-?
之前相反, 它是用来把属性变成可选项的。
Pick
它的作用是从T
中取出一系列K
的属性
type Pick<T, K extends keyof T> = {
[P in K]: T[P] };
type PickUser = Pick<User, "age" | "name">
Exclude
T extends U ? X : Y
以上语句的意思就是 如果 T
是 U
的子类型的话,那么就会返回 X
,否则返回 Y
type Exclude<T,U> = T extends U ?never : T;
const str: Exclude<'a' | '1' | '2', 'a' | 'y' | 'z'> = '1';
从代码的提示,可以很好的理解,Exclude
就是将前面类型的与后面类型对比,过滤出前面独有的属性
Omit
// Pick
type Pick<T, K extends keyof T> = {
[P in K]: T[P] };
// Exclude
type Exclude<T, U> = T extends U ? never : T;
// Omit
type Omit = Pick<T, Exclude<keyof T, K>>
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
interface User {
id: number;
age: number;
name: string;
}
// 相当于: type OmitUser = { age: number; name: string; }
type OmitUser = Omit<User, 'id'>; // 很好理解,去除掉 User 接口内的 id 属性。
还有…就不多作坠叙了
函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
function 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) {
// type Combinable = string | number;
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
在以上代码中,我们为 add
函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript
中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载
是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class 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("Semlinker", " Kakuqo");
这里需要注意的是,当 TypeScript
编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator
类中,add(a: Combinable, b: Combinable){ }
并不是重载列表的一部分,因此对于 add
成员方法来说,我们只定义了四个重载方法。
注意点:
- 接口不能实现接口或者类,所以实现只能用于类身上,即类可以实现接口或类
- 接口可以继承接口或类
- 类不可以继承接口,类只能继承类
- 可多继承或者多实现
TypeScript中的类如何实现接口
对类的实例部分进行约束
在 TypeScript
中,可通过接口Interface
对js
中的类Class
进行约束,要求被约束的类具有一定的结构(具有特定类型的属性与方法),使得项目开发更规范,提高代码的可读性与可维护性。
// 首先声明一个DogInterface接口,
// 这个接口要求被约束的内容,其内部要有
// 一个string类型的name属性、
// 以及一个不返回内容的say方法
interface DogInterface {
name: string;
say(): void;
}
// 然后声明一个Dog类,这个类实现了DogInterface接口。
// 这要求Dog类中要具备DogInterface接口所述的
// 所有属性、方法,否则就会报错
class Dog implements DogInterface {
name: string;
say(): void {
console.log(`${
this.name}wang wang wang`);
}
constructor(name: string) {
this.name = name;
}
}
// 之后这个Dog可以正常地使用了
let dog = new Dog("旺财");
dog.say(); // '旺财wang wang wang'
上面这个例子展示了如何让一个类实现某个接口