1. any 类型,unknown 类型,never 类型
TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个
1、any
1.1 基本含义
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
let x:any;
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确
TypeScript 认为,只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。
从集合论的角度看,any类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。
1.1.1 类型推断问题
对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any。
function add(x, y) {
return x + y;
}
add(1, [1, 2, 3]) // 不报错
上面示例中,函数add()的参数变量x和y,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any。以至于后面就不再对函数add()进行类型检查了,怎么用都可以。这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any。
当声明的时候没有赋值,没给类型也不会报错,所以在声明的同时要赋值,不然会存在安全隐患
1.1.2污染问题
ny类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错
let x:any = 'hello';
let y:number;
y = x; // 不报错
y * 123 // 不报错
y.toFixed() // 不报错
上面示例中,变量x的类型是any,实际的值是一个字符串。变量y的类型是number,表示这是一个数值变量,但是它被赋值为x,这时并不会报错。然后,变量y继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。
污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any类型的另一个主要原因。
2.unknown
与any含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any那样自由,可以视为严格版的any
unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制:
1.unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)
let v:unknown = 123;
let v1:boolean = v; // 报错
let v2:number = v; // 报错
2. 不能直接调用unknown
类型变量的方法和属性。
let v1:unknown = { foo: 123 };
v1.foo // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错
3. unknown
类型变量能够进行的运算是有限的,只能进行比较运算(运算符==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符!
)、typeof
运算符和instanceof
运算符这几种,其他运算都会报错
let a:unknown = 1;
a + 1 // 报错
a === 1 // 正确
4. 经过“类型缩小”,unknown
类型变量才可以使用。所谓“类型缩小”,就是缩小unknown
变量的类型范围,确保不会出错。
let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}
unknown可以看作是更安全的any。一般来说,凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。也视为所有其他类型(除了any)的全集,所以它和any一样,也属于 TypeScript 的顶层类型。
3.never
由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。
never类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性。另外,不可能返回值的函数,返回值的类型就可以写成never
如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。
function fn(x:string|number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}
never
类型的一个重要特点是,可以赋值给任意其他类型。
function f():never {
throw new Error('Error');
}
let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错
上面示例中,函数f()
会抛错,所以返回值类型可以写成never
,即不可能返回任何值。各种其他类型的变量都可以赋值为f()
的运行结果(never
类型)
2.类型系统
1.基本类型(继承js基本数据类型)
boolean
string
number
bigint
symbol
object
undefined
null
注意:
1.上面所有类型的名称都是小写字母,首字母大写的Number、String、Boolean等在 JavaScript 语言中都是内置对象,而不是类型名称
2.如果没有声明类型的变量,被赋值为undefined或null,它们的类型会被推断为any,如果希望避免这种情况,则需要打开编译选项strictNullChecks
2.包装对象类型
TypeScript 对五种原始类型分别提供了大写和小写两种类型:
Boolean 和 boolean
String 和 string
Number 和 number
BigInt 和 bigint
Symbol 和 symbol
大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错
String类型可以赋值为字符串的字面量,也可以赋值为包装对象。但是,string类型只能赋值为字面量,赋值为包装对象就会报错。(只使用小写类型,不使用大写类型)
3.Object 类型与 object 类型
3.1Oject类型
大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。(原始类型值、对象、数组、函数都是合法的Object类型)
let obj:Object;//与let obj:{}一样,只不过后者是简写
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
除了undefined
和null
这两个值不能转为对象,其他任何值都可以赋值给Object
类型
let obj:Object;
obj = undefined; // 报错
obj = null; // 报错
3.2object类型
小写的object
类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象(包含对象、数组和函数,不包括原始类型的值)
let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object,不使用大写类型Object
注意,无论是大写的Object类型,还是小写的object类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中
4.undefined和null的特殊性
undefined和null既是值,又是类型
作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefined或null。
JavaScript 的行为是,变量如果等于undefined就表示还没有赋值,如果等于null就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。
let age:number = 24;
age = null; // 报错
age = undefined; // 报错
上面示例中,打开--strictNullChecks
以后,number
类型的变量age
就不能赋值为undefined
和null
。
这个选项在配置文件tsconfig.json
的写法如下。
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}
打开strictNullChecks
以后,undefined
和null
这两种值也不能互相赋值了。undefined
和null
只能赋值给自身,或者any
类型和unknown
类型的变量
let x:any = undefined;
let y:unknown = null;
5.联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|
表示。
联合类型A|B
表示,任何一个类型只要属于A
或B
,就属于联合类型A|B
。
let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确
6.交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&
表示
交叉类型的主要用途是表示对象的合成:
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};
上面示例中,变量obj
同时具有属性foo
和属性bar
。
交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };
上面示例中,类型B
是一个交叉类型,用来在A
的基础上增加了属性bar
。
7.type命令
type
命令用来定义一个类型的别名。
type Age = number;
let age:Age = 55;
上面示例中,type
命令为number
类型定义了一个别名Age
。这样就能像使用number
一样,使用Age
作为类型。
8.typeof运算符
TypeScript 将typeof
运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
上面示例中,typeof a表示返回变量a的 TypeScript 类型({ x: number })。同理,typeof a.x返回的是属性x的类型(number)。
这种用法的typeof返回的是 TypeScript 类型,所以只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算
type T = typeof Date(); // 报错
上面示例会报错,原因是 typeof 的参数不能是一个值的运算式,而Date()
需要运算才知道结果。
另外,typeof
命令的参数不能是类型。
type Age = number;
type MyAge = typeof Age; // 报错
上面示例中,Age是一个类型别名,用作typeof命令的参数就会报错。
typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量foo的类型,这时使用typeof foo就可以获得它的类型。
9.块级类型声明
TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效
if (true) {
type T = number;
let v:T = 5;
} else {
type T = string;
let v:T = 'hello';
}
10.类型的兼容
type T = number|string;
let a:number = 1;
let b:T = a;
变量a
和b
的类型是不一样的,但是变量a
赋值给变量b
并不会报错。这时,我们就认为,b
的类型兼容a
的类型。
如果类型A
的值可以赋值给类型B
,那么类型A
就称为类型B
的子类型(subtype)。在上例中,类型number
就是类型number|string
的子类型
let a:'hi' = 'hi';
let b:string = 'hello';
b = a; // 正确
a = b; // 报错
上面示例中,hi
是string
的子类型,string
是hi
的父类型。所以,变量a
可以赋值给变量b
,但是反过来就会报错。
子类型继承了父类型的所有特征,所以可以用在父类型的场合。但是,子类型还可能有一些父类型没有的特征,所以父类型不能用在子类型的场合。
3.数组
TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员
声明的方式:
let arr:number[] = [1, 2, 3];
let arr:(number|string)[];
let arr:Array<number> = [1, 2, 3];
数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。
let arr:number[];
arr = [];
arr = [1];
arr = [1, 2];
arr = [1, 2, 3];
let arr1:number[] = [1, 2, 3];
arr1[3] = 4;
arr1.length = 2;
arr1// [1, 2]
上面示例中,数组arr无论有多少个成员,都是正确的。
这种规定的隐藏含义就是,数组的成员是可以动态变化的,数组增加成员或减少成员,都是可以的。
1.类型推断
如果数组变量没有声明类型,TypeScript 就会推断数组成员的类型。这时,推断行为会因为值的不同,而有所不同。后面,为这个数组赋值时,TypeScript 会自动更新类型推断。
const arr = [];
arr // 推断为 any[]
arr.push(123);
arr // 推断类型为 number[]
arr.push('abc');
arr // 推断类型为 (string|number)[]
但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错
上面示例中,数组变量arr
的初始值是[123]
,TypeScript 就推断成员类型为number
。新成员如果不是这个类型,TypeScript 就会报错,而不会更新类型推断。
2.只读数组,const断言
TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly
关键字。arr
是一个只读数组,删除、修改、新增数组成员都会报错。
const arr:readonly number[] = [0, 1];
arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错
TypeScript 将readonly number[]
与number[]
视为两种不一样的类型,后者是前者的子类型。
所以子类型number[]
可以用于所有使用父类型的场合,反过来就不行。
let a1:number[] = [0, 1];
let a2:readonly number[] = a1; // 正确
a1 = a2; // 报错
只读数组还有一种声明方法,就是使用“const 断言”。
const arr = [0, 1] as const;
arr[0] = [2]; // 报错
上面示例中,as const
告诉 TypeScript,推断类型时要把变量arr
推断为只读数组,从而使得数组成员无法改变。
3.多维数组
var multi:number[][] =
[[1,2,3], [23,24,25]];
4.元组
它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同
数组的成员类型写在方括号外面(number[]
),元组的成员类型是写在方括号里面([number]
// 数组
let a:number[] = [1];
// 元组
let t:[number] = [1];
使用元组时,必须明确给出类型声明(上例的[number]
),不能省略,否则 TypeScript 会把一个值自动推断为数组。
// a 的类型被推断为 (number | boolean)[]
let a = [1, true];
元组成员的类型可以添加问号后缀(?
),表示该成员是可选的。所有可选成员必须在必选成员之后。
let a:[number, number?] = [1];
由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。
let x:[string, string] = ['a', 'b'];
x[2] = 'c'; // 报错
但是,使用扩展运算符(...
),可以表示不限成员数量的元组。
type NamedNums = [
string,
...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];
1.只读元组
元组也可以是只读的,不允许修改,有两种写法。
// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>
跟数组一样,只读元组是元组的父类型。所以,元组可以替代只读元组,而只读元组不能替代元组。
type t1 = readonly [number, number];
type t2 = [number, number];
let x:t2 = [1, 2];
let y:t1 = x; // 正确
x = y; // 报错
2、成员数量推断
function f(point: [number, number]) {
if (point.length === 3) { // 报错
// ...
}
}
上面示例会报错,原因是 TypeScript 发现元组point
的长度是2
,不可能等于3
,这个判断无意义
如果包含了可选成员,TypeScript 会推断出可能的成员数量。
function f(
point:[number, number?, number?]
) {
if (point.length === 4) { // 报错
// ...
}
}
如果使用了扩展运算符,TypeScript 就无法推断出成员数量。
const myTuple:[...string[]]
= ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
// ...
}
一旦扩展运算符使得元组的成员数量无法推断,TypeScript 内部就会把该元组当成数组处理
3.扩展运算符与成员数量
如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。
const arr = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 报错
解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。
const arr:[number, number] = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 正确
5.symbol类型
Symbol 值通过Symbol()
函数生成。在 TypeScript 里面,Symbol 的类型使用symbol
表示。
let x:symbol = Symbol();
let y:symbol = Symbol();
x === y // false
上面示例中,变量x
和y
的类型都是symbol
,且都用Symbol()
生成,但是它们是不相等的。
1.unique symbol
symbol
的一个子类型unique symbol
,它表示单个的、某个具体的 Symbol 值。
因为unique symbol
表示单个值,所以这个类型的变量是不能修改值的,只能用const
命令声明,不能用let
声明。
// 正确
const x:unique symbol = Symbol();
// 报错
let y:unique symbol = Symbol();
const x:unique symbol = Symbol();
// 等同于
const x = Symbol();
每个声明为unique symbol
类型的变量,它们的值都是不一样的,其实属于两个值类型。
const a:unique symbol = Symbol();
const b:unique symbol = Symbol();
a === b // 报错
变量a
和b
是两个类型,就不能把一个赋值给另一个。
const a:unique symbol = Symbol();
const b:unique symbol = a; // 报错
如果要写成与变量a
同一个unique symbol
值类型,只能写成类型为typeof a
。
const a:unique symbol = Symbol();
const b:typeof a = a; // 正确
unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行。
const a:unique symbol = Symbol();
const b:symbol = a; // 正确
const c:unique symbol = b; // 报错
2.类型推断
let
命令声明的变量,推断类型为 symbol。
// 类型为 symbol
let x = Symbol();
const
命令声明的变量,推断类型为 unique symbol。
// 类型为 unique symbol
const x = Symbol();
但是,const
命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。
let x = Symbol();
// 类型为 symbol
const y = x;
let
命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。
const x = Symbol();
// 类型为 symbol
let y = x;
6.函数类型
数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
function hello(
txt:string
):void {
console.log('hello ' + txt);
}
上面示例中,函数hello()在声明时,需要给出参数txt的类型(string),以及返回值的类型(void),后者写在参数列表的圆括号后面。void类型表示没有返回值(返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。)
如果变量被赋值为一个函数,变量的类型有两种写法。
// 写法一
const hello = function (txt:string) {
console.log('hello ' + txt);
}
// 写法二
const hello:
(txt:string) => void
= function (txt) {
console.log('hello ' + txt);
};
如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type
命令为函数类型定义一个别名,便于指定给其他变量。
type MyFunc = (txt:string) => void;
const hello:MyFunc = function (txt) {
console.log('hello ' + txt);
};
函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数。
let myFunc:
(a:number, b:number) => number;
myFunc = (a:number) => a; // 正确
myFunc = (
a:number, b:number, c:number
) => a + b + c; // 报错
如果一个变量要套用另一个函数类型,有一个小技巧,就是使用typeof
运算符。
function add(
x:number,
y:number
) {
return x + y;
}
const myAdd:typeof add = function (x, y) {
return x + y;
}
1.Function类型
TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。(不建议这样使用,不然传入和传出的都是any类型)
function doSomething(f:Function) {
return f(1, 2, 3);
}
2.箭头函数
箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。
const repeat = (
str:string,
times:number
):string => str.repeat(times);
看一个例子。
type Person = { name: string };
const people = ['alice', 'bob', 'jan'].map(
(name):Person => ({name})
);
上面示例中,Person是一个类型别名,代表一个对象,该对象有属性name。变量people是数组的map()方法的返回值。
map()方法的参数是一个箭头函数(name):Person => ({name}),该箭头函数的参数name的类型省略了,因为可以从map()的类型定义推断出来,箭头函数的返回值类型为Person。相应地,变量people的类型是Person[]。
至于箭头后面的({name}),表示返回一个对象,该对象有一个属性name,它的属性值为变量name的值。这里的圆括号是必须的,否则(name):Person => {name}的大括号表示函数体,即函数体内有一行语句name,同时由于没有return语句,这个函数不会返回任何值。
3.可选参数
如果函数的某个参数可以省略,则在参数名后面加问号表示。(参数名带有问号,表示该参数的类型实际上是原始类型|undefined
,它有可能为undefined
)
function f(x?:number) {
// ...
}
f(); // OK
f(10); // OK
函数体内部用到可选参数时,需要判断该参数是否为undefined
。
let myFunc:
(a:number, b?:number) => number;
myFunc = function (x, y) {
if (y === undefined) {
return x;
}
return x + y;
}
上面示例中,由于函数的第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空。
4.参数默认值
设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。(可选参数与默认值不能同时使用。)
function createPoint(
x:number = 0,
y:number = 0
):[number, number] {
return [x, y];
}
createPoint() // [0, 0]
具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入undefined
。
function add(
x:number = 0,
y:number
) {
return x + y;
}
add(1) // 报错
add(undefined, 1) // 正确
5.参数解构
函数参数如果存在变量解构,类型写法如下。
function f(
[x, y]: [number, number]
) {
// ...
}
function sum(
{ a, b, c }: {
a: number;
b: number;
c: number
}
) {
console.log(a + b + c);
}
参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。
type ABC = { a:number; b:number; c:number };
function sum({ a, b, c }:ABC) {
console.log(a + b + c);
}
6.rest参数
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。
// rest 参数为数组
function joinNumbers(...nums:number[]) {
// ...
}
// rest 参数为元组
function f(...args:[boolean, number]) {
// ...
}
下面是一个 rest 参数的例子。
function multiply(n:number, ...m:number[]) {
return m.map((x) => n * x);
}
上面示例中,参数m
就是 rest 类型,它的类型是一个数组。
rest 参数甚至可以嵌套。
function f(...args:[boolean, ...string[]]) {
// ...
}
rest 参数可以与变量解构结合使用。
function repeat(
...[str, times]: [string, number]
):string {
return str.repeat(times);
}
// 等同于
function repeat(
str: string,
times: number
):string {
return str.repeat(times);
}
7.readonly只读参数
如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上readonly
关键字,表示这是只读参数。
function arraySum(
arr:readonly number[]
) {
// ...
arr[0] = 0; // 报错
}
8.void类型
void 类型表示函数没有返回值。(如果返回其他值,就会报错,但是允许返回null和undefined,如果打开了strictNullChecks编译选项,那么 void 类型只允许返回undefined。如果返回null,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回undefined。)
function f():void {
console.log('hello');
}
需要特别注意的是,如果变量、对象方法、函数参数的类型是 void 类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。
type voidFunc = () => void;
const f:voidFunc = () => {
return 123;
};
这是因为,这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。
const src = [1, 2, 3];
const ret = [];
src.forEach(el => ret.push(el));
上面示例中,push()
有返回值,表示新插入的元素在数组里面的位置。但是,对于forEach()
方法来说,这个返回值是没有作用的,根本用不到,所以 TypeScript 不会报错。
9.never类型
never
类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。
它主要有以下两种情况。
(1)抛出错误的函数。
function fail(msg:string):never {
throw new Error(msg);
}
上面示例中,函数fail()
会抛错,不会正常退出,所以返回值类型是never
。
注意,只有抛出错误,才是 never 类型。如果显式用return
语句返回一个 Error 对象,返回值就不是 never 类型。
function fail():Error {
return new Error("Something failed");
}
上面示例中,函数fail()
返回一个 Error 对象,所以返回值类型是 Error。
(2)无限执行的函数。
const sing = function():never {
while (true) {
console.log('sing');
}
};
注意,never
类型不同于void
类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回undefined
。
如果程序中调用了一个返回值类型为never
的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。
10.局部类型
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。
function hello(txt:string) {
type message = string;
let newTxt:message = 'hello ' + txt;
return newTxt;
}
const newTxt:message = hello('world'); // 报错
上面示例中,类型message是在函数hello()内部定义的,只能在函数内部使用。在函数外部使用,就会报错。
11.函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]
分别对函数reverse()
的两种参数情况,给予了类型声明。但是,到这里还没有结束,后面还必须对函数reverse()
给予完整的类型声明。
function reverse(str:string):string;
function reverse(arr:any[]):any[];
function reverse(
stringOrArray:string|any[]
):string|any[] {
if (typeof stringOrArray === 'string')
return stringOrArray.split('').reverse().join('');
else
return stringOrArray.slice().reverse();
}
函数体内部需要判断参数的类型及个数,并根据判断结果执行不同的操作。
function add(
x:number,
y:number
):number;
function add(
x:any[],
y:any[]
):any[];
function add(
x:number|any[],
y:number|any[]
):number|any[] {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
} else if (Array.isArray(x) && Array.isArray(y)) {
return [...x, ...y];
}
throw new Error('wrong parameters');
}
重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。
function f(x:any):number;
function f(x:string): 0|1;
function f(x:any):any {
// ...
}
const a:0|1 = f('hi'); // 报错
上面声明中,第一行类型声明x:any
范围最宽,导致函数f()
的调用都会匹配这行声明,无法匹配第二行类型声明,所以最后一行调用就报错了,因为等号两侧类型不匹配,左侧类型是0|1
,右侧类型是number
。这个函数重载的正确顺序是,第二行类型声明放到第一行的位置
7.对象
1.可选属性
如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。(可选属性等同于允许赋值为undefined
)
const obj: {
x: number;
y?: number;
} = { x: 1 };
/ 写法一
let firstName = (user.firstName === undefined)
? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined)
? 'Bar' : user.lastName;
// 写法二
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';
上面示例中,写法一使用三元运算符?:,判断是否为undefined,并设置默认值。写法二使用 Null 判断运算符??,与写法一的作用完全相同。
TypeScript 提供编译设置ExactOptionalPropertyTypes,只要同时打开这个设置和strictNullChecks,可选属性就不能设为undefined
// 打开 ExactOptionsPropertyTypes 和 strictNullChecks
const obj: {
x: number;
y?: number;
} = { x: 1, y: undefined }; // 报错
上面示例中,打开了这两个设置以后,可选属性就不能设为undefined
了。
注意,可选属性与允许设为undefined
的必选属性是不等价的。
type A = { x:number, y?:number };
type B = { x:number, y:number|undefined };
const ObjA:A = { x: 1 }; // 正确
const ObjB:B = { x: 1 }; // 报错
上面示例中,属性y
如果是一个可选属性,那就可以省略不写;如果是允许设为undefined
的必选属性,一旦省略就会报错,必须显式写成{ x: 1, y: undefined }
2.只读属性
属性名前面加上readonly
关键字,表示这个属性是只读属性,不能修改。
interface MyInterface {
readonly prop: number;
}
注意,如果属性值是一个对象,readonly
修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。
interface Home {
readonly resident: {
name: string;
age: number
};
}
const h:Home = {
resident: {
name: 'Vicky',
age: 42
}
};
h.resident.age = 32; // 正确
h.resident = {
name: 'Kate',
age: 23
} // 报错
如果希望属性值是只读的,除了声明时加上readonly
关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言a
const myUser = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 报错
注意,上面的as const
属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。
const myUser:{ name: string } = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 正确
3.属性名的索引类型
type MyObj = {
[property: string]: string
};
const obj:MyObj = {
foo: 'a',
bar: 'b',
baz: 'c',
};
上面示例中,类型MyObj的属性名类型就采用了表达式形式,写在方括号里面。[property: string]的property表示属性名,这个是可以随便起的,它的类型是string,即属性名类型为string。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。
4.解构赋值
解构赋值用于直接从对象中提取属性。
const {id, name, price} = product;
上面语句从对象product
提取了三个属性,并声明属性名的同名变量。
5.结构类型原则
只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。
type A = {
x: number;
};
type B = {
x: number;
y: number;
};
对象A
只有一个属性x
,类型为number
。对象B
满足这个特征,因此兼容对象A
,只要可以使用A
的地方,就可以使用B
。
const B = {
x: 1,
y: 1
};
const A:{ x: number } = B; // 正确
8.interface
1.interface对象的5种语法
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
(1)对象属性
interface Point {
x: number;
y: number;
}
上面示例中,x
和y
都是对象的属性,分别使用冒号指定每个属性的类型。
(2)对象的属性索引
interface A {
[prop: string]: number;
}
上面示例中,[prop: string]
就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
(3)对象的方法
对象的方法共有三种写法。
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boole
(4)函数
interface 也可以用来声明独立的函数。
interface Add {
(x:number, y:number): number;
}
const myAdd:Add = (x,y) => x + y;
上面示例中,接口Add
声明了一个函数类型。
(5)构造函数
interface 内部可以使用new
关键字,表示构造函数。
interface ErrorConstructor {
new (message?: string): Error;
}
2.interface的继承
2.1.interface继承interface
interface 可以使用extends
关键字,继承其他 interface。
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
上面示例中,Circle
继承了Shape
,所以Circle
其实有两个属性name
和radius
。这时,Circle
是子接口,Shape
是父接口。
extends
关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。
interface 允许多重继承。
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}
上面示例中,Circle
同时继承了Style
和Shape
,所以拥有三个属性color
、name
和radius
。
多重接口继承,实际上相当于多个父接口的合并
2.2interface继承type
interface 可以继承type
命令定义的对象类型。i
type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
}
上面示例中,CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性。
注意,如果type命令定义的类型不是对象,interface 就无法继承。
2.3interface继承class
interface 还可以继承 class,即继承该类的所有成员。
class A {
x:string = '';
y():boolean {
return true;
}
}
interface B extends A {
z: number
}
上面示例中,B
继承了A
,因此B
就具有属性x
、y()
和z
。
实现B
接口的对象就需要实现这些属性。
const b:B = {
x: '',
y: function(){ return true },
z: 123
}
上面示例中,对象b
就实现了接口B
,而接口B
又继承了类A
。
3.接口合并
多个同名接口会合并成一个接口。
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
上面示例中,两个Box接口会合并成一个接口,同时有height、width和length三个属性。
4.interface与type的异同
interface命令与type命令作用类似,都可以表示对象类型。
很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
它们的相似之处,首先表现在都能为对象类型起名。
type Country = {
name: string;
capital: string;
}
interface Coutry {
name: string;
capital: string;
}
上面示例是type命令和interface命令,分别定义同一个类型。
4.1interface与type的区别:
(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。
(2)interface可以继承其他类型,type不支持继承。
继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
上面示例中,类型Bear
在Animal
的基础上添加了一个属性honey
。上例的&
运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。
作为比较,interface
添加属性,采用的是继承的写法。
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
继承时,type 和 interface 是可以换用的。interface 可以继承 type。
type Foo = { x: number; };
interface Bar extends Foo {
y: number;
}
type 也可以继承 interface。
interface Foo {
x: number;
}
type Bar = Foo & { y: number; };
(3)同名interface
会自动合并,同名type
则会报错。也就是说,TypeScript 不允许使用type
多次定义同一个类型。
type A = { foo:number }; // 报错
type A = { bar:number }; // 报错
上面示例中,type
两次定义了类型A
,导致两行都会报错。
(4)interface
不能包含属性映射(mapping),type
可以
interface Point {
x: number;
y: number;
}
// 正确
type PointCopy1 = {
[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
[Key in keyof Point]: Point[Key];
};
(5)this
关键字只能用于interface
。
// 正确
interface Foo {
add(num:number): this;
};
// 报错
type Foo = {
add(num:number): this;
};
6)type 可以扩展原始数据类型,interface 不行。
// 正确
type MyStr = string & {
type: 'new'
};
// 报错
interface MyStr extends string {
type: 'new'
}
(7)interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。
type A = { /* ... */ };
type B = { /* ... */ };
type AorB = A | B;
type AorBwithName = AorB & {
name: string
};
上面示例中,类型AorB是一个联合类型,AorBwithName则是为AorB添加一个属性。这两种运算,interface都没法表达。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用