TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了:类型系统
TypeScript 类型系统的主要优势:可以显示标记出代码中的意外行为,从而降低了发生错误的可能性
示例代码:
let age = 18
let age: number = 18
: number 就是类型注解// 错误代码:
// 错误原因:将 string 类型的值赋值给了 number 类型的变量,类型不一致
let age: number = '18'
可以将 TS 中的常用基础类型细分为两类:
string、boolean、number、object、bigint、symbol、null 和 undefined)原始类型:string、boolean、number、bigint、object、symbol、null 和 undefined
特点:简单,这些类型,完全按照 JS 中类型的名称来书写
number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;
boolean
let isDone: boolean = false;
string
let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}.
I'll be ${age + 1} years old next month.`;
其它
let a: null = null
let b: undefined = undefined
let obj: object = {x: 1}
let s: symbol = Symbol()
对象类型:Object(包括,数组、对象、函数等对象)。
特点:对象类型,在TS中更加细化,每个具体的对象都有自己的类型语法。
null和undefined
默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给其他类型。
// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined
// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined
// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined
// null和undefined赋值给Symbol
let sym: symbol = Symbol("me");
sym = null
sym= undefined
// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined
// null和undefined赋值给bigint
let big: bigint = 100n;
big = null
big= undefined
如果你在tsconfig.json指定了"strictNullChecks":true ,null 和 undefined 只能赋值给 void 和它们各自的类型。
number和bigint
虽然number和bigint都表示数字,但是这两个类型不兼容。
let big: bigint = 100n;
let num: number = 6;
big = num;
num = big;
会抛出一个类型不兼容的 ts(2322) 错误。
数组类型的两种写法:(推荐使用number[]写法)
let numbers: number[] = [1,2,3]
let string: Array<string> = ["a","b","c"]
定义联合类型数组
let arr:(number | string)[];
// 表示定义了一个名称叫做arr的数组,
// 这个数组中将来既可以存储数值类型的数据, 也可以存储字符串类型的数据
arr3 = [1, 'b', 2, 'c'];
定义指定对象成员的数组:
// interface是接口,最后面会讲到
interface Arrobj{
name:string,
age:number
}
let arr3:Arrobj[]=[{name:'jimmy',age:22}]
联合类型表示取值可以为多种类型中的一种,使用 | 分隔每个类型。
需求:数组中既有 number 类型,又有 string 类型,这个数组的类型应该如何写?
let arr: (number | string)[] = [1, 'a', 3, 'b']
|(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种 let timer: number | null = null
timer = setInterval(() => {}, 1000)
// 定义一个数组,数组中可以有数字或者字符串, 需要注意 | 的优先级
let arr: (number | string)[] = [1, 'abc', 2]
联合类型通常与 null 或 undefined 一起使用:
const sayHello = (name: string | undefined) => {
/* ... */
};
例如,这里 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给sayHello 函数。
sayHello("semlinker");
sayHello(undefined);
通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
以上示例中的 1、2 或 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个。
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用&定义交叉类型。
{
type Useless = string & number;
}
很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Useless 的类型就是个 never。
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:
type IntersectionType = { id: number; name: string; } & { age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
在上述示例中,我们通过交叉类型,使得 IntersectionType 同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。
类型别名(自定义类型):类型别名用来给一个类型起个新名字。(类型别名常用于联合类型。)type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
type 关键字来创建自定义类型注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
函数参数和返回值的类型// 函数声明
function add(a: number, b: number): number {
return a + b
}
// 函数表达式
let mySum: (x: number, y: number) => number = function (x, y) {
return x + y;
};
// 箭头函数
const add = (a: number, b: number): number => {
return a + b
}
type AddFn = (a: number, b: number) => number
const add: AddFn = (a, b) => {
return a + b
}
interface SearchFunc{
(source: string, subString: string): boolean;
}
采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
void表示没有任何类型,和其他类型是平等关系,不能直接赋值:
let a: void;
let b: number = a; // Error
你只能为它赋予null和undefined(在strictNullChecks未指定为true时)。声明一个void类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。
如果函数没有返回值,那么,函数返回值类型为:void
function greet(name: string): void {
console.log('Hello', name)
}
undefined,但是我们需要定义成void类型,而不是undefined类型。否则将报错:// 如果什么都不写,此时,add 函数的返回值类型为: void
const add = () => {}
// 这种写法是明确指定函数返回值类型为 void,与上面不指定返回值类型相同
const add = (): void => {}
function fun(): undefined {
console.log("this is TypeScript");
};
fun(); // Error
// 但,如果指定 返回值类型为 undefined,此时,函数体中必须显示的 return undefined 才可以
const add = (): undefined => {
// 此处,返回的 undefined 是 JS 中的一个值
return undefined
}
值会永不存在的两种情况:
// 异常
function err(msg: string): never { // OK
throw new Error(msg);
}
// 死循环
function loopForever(): never { // OK
while (true) {};
}
never类型同null和undefined一样,也是任何类型的子类型,也可以赋值给任何类型。
但是没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使any也不可以赋值给never
let ne: never;
let nev: never;
let an: any;
ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => { throw new Error("异常"); })(); // OK
ne = (() => { while(true) {} })(); // OK
在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
type 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 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
比较:void和never
// void 用来表示空,以函数为例,就表示没有返回值的函数
function fn(): void {
console.log('hello');
return undefined;
return;
}
// never 表示永远不会返回结果
function fn1(): never {
throw new Error('出错啦!');
}
slice() 也可以 slice(1) 还可以 slice(1, 3)function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
?(问号)function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
{
let str: string = 'this is string';
let num: number = 1;
let bool: boolean = true;
}
{
const str: string = 'this is string';
const num: number = 1;
const bool: boolean = true;
}
看着上面的示例,可能你已经在嘀咕了:定义基础类型的变量都需要写明类型注解,TypeScript 太麻烦了吧?在示例中,使用 let 定义变量时,我们写明类型注解也就罢了,毕竟值可能会被改变。可是,使用 const 常量时还需要写明类型注解,那可真的很麻烦。
实际上,TypeScript 早就考虑到了这么简单而明显的问题。
{
let str = 'this is string'; // 等价
let num = 1; // 等价
let bool = true; // 等价
}
{
const str = 'this is string'; // 不等价
const num = 1; // 不等价
const bool = true; // 不等价
}
我们能根据 return 语句推断函数返回的类型,如下代码所示:
{
/** 根据参数的类型,推断出返回值的类型也是 number */
function add1(a: number, b: number) {
return a + b;
}
const x1= add1(1, 1); // 推断出 x1 的类型也是 number
/** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
function add2(a: number, b = 1) {
return a + b;
}
const x2 = add2(1);
const x3 = add2(1, '1'); // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined
}
我们把 TypeScript 这种基于赋值表达式推断类型的能力称之为类型推断。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成
any类型而完全不被类型检查:let myFavoriteNumber; myFavoriteNumber = 'seven'; myFavoriteNumber = 7;
- 1
- 2
- 3
// 空对象
let person: {} = {}
// 有属性的对象
let person: { name: string } = {
name: '同学'
}
// 既有属性又有方法的对象
// 在一行代码中指定对象的多个属性类型时,使用 `;`(分号)来分隔
let person: { name: string; sayHi(): void } = {
name: 'jack',
sayHi() {}
}
// 对象中如果有多个类型,可以换行写:
// 通过换行来分隔多个属性类型,可以去掉 `;`
let person: {
name: string
sayHi(): void
} = {
name: 'jack',
sayHi() {}
}
{} 来描述对象结构属性名: 类型的形式方法名(): 返回值类型的形式{
greet(name: string):string
greet: (name: string) => string
}
type Person = {
greet: (name: string) => void
greet(name: string):void
}
let person: Person = {
greet(name) {
console.log(name)
}
}
axios({ ... }) 时,如果发送 GET 请求,method 属性就可以省略? 来表示type Config = {
url: string
method?: string
}
function axios(config: Config) {
console.log(config)
}
{} 形式为对象添加类型,会降低代码的可读性(不好辨识类型和值)// 创建类型别名
type Person = {
name: string
sayHi(): void
}
// 使用类型别名作为对象的类型:
let person: Person = {
name: 'jack',
sayHi() {}
}
有时候我们希望一个对象中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
// [propName: string]: any 表示任意类型的属性
let c: {name: string, [propName: string]: any};
c = {name: '猪八戒', age: 18, gender: '男'};
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。
let position: number[] = [116.2317, 39.5427]
元组 Tuple// 类型必须匹配且个数必须为2
let position: [number, number] = [39.5427, 116.2317]
注意,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。如果一个数组中可能有多种类型,数量和类型都不确定,那就直接any[]
我们可以通过下标的方式来访问元组中的元素,当元组中的元素较多时,这种方式并不是那么便捷。其实元组也是支持解构赋值的:
let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${id}`);
console.log(`username: ${username}`);
以上代码成功运行后,控制台会输出以下消息:
id: 1
username: Semlinker
这里需要注意的是,在解构赋值时,解构数组元素的个数是不能超过元组中元素的个数,否则也会出现错误,比如:
let employee: [number, string] = [1, "Semlinker"];\
let [id, username, age] = employee;
在以上代码中,我们新增了一个 age 变量,但此时 TypeScript 编译器会提示以下错误信息:
Tuple type '[number, string]' of length '2' has no element at index '2'.
很明显元组类型 [number, string] 的长度是 2,在位置索引 2 处不存在任何元素。
与函数签名类型,在定义元组类型时,我们也可以通过 ? 号来声明元组类型的可选元素,具体的示例如下:
let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${optionalTuple}`);
在上面代码中,我们定义了一个名为 optionalTuple 的变量,该变量的类型要求包含一个必须的字符串属性和一个可选布尔属性,该代码正常运行后,控制台会输出以下内容:
optionalTuple : Semlinker,true
optionalTuple : Kakuqo
那么在实际工作中,声明可选的元组元素有什么作用?这里我们来举一个例子,在三维坐标轴中,一个坐标点可以使用 (x, y, z) 的形式来表示,对于二维坐标轴来说,坐标点可以使用 (x, y) 的形式来表示,而对于一维坐标轴来说,只要使用 (x) 的形式来表示即可。针对这种情形,在 TypeScript 中就可以利用元组类型可选元素的特性来定义一个元组类型的坐标点,具体实现如下:
type Point = [number, number?, number?];
const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点
console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3
元组类型里最后一个元素可以是剩余元素,形式为 ...X,这里 X 是数组类型。剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string[]] 表示带有一个 number 元素和任意数量string 类型元素的元组类型。为了能更好的理解,我们来举个具体的例子:
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);
TypeScript 3.4 还引入了对只读元组的新支持。我们可以为任何元组类型加上 readonly 关键字前缀,以使其成为只读元组。具体的示例如下:
const point: readonly [number, number] = [10, 20];
在使用 readonly 关键字修饰元组类型之后,任何企图修改元组中元素的操作都会抛出异常:
// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);