这种问题一般是一个对象类型,但是并没有给ts声明它包含了哪些属性,当使用的时候就会给出这种错误提示
function reportEvent(type, data = {}){
const event = `${data.actionName}`;
let value = JSON.stringify(data.extra);
上面对于传递过来的参数data
并没有指定对象里有哪些属性,所以后面使用data.actionName
和data.extra
的时候就会报错。可如下手动指定data
包含的属性
function reportEvent(type, data:{actionName: string, extra: any} = {actionName: '', extra: undefined}){
const event = `${data.actionName}`;
let value = JSON.stringify(data.extra);
或者省事可以使用any
属性
function reportEvent(type, data: any = {}){
const event = `${data.actionName}`;
let value = JSON.stringify(data.extra);
JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。
布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean
定义布尔值类型:
let learning: boolean = false;
但是!使用构造函数 Boolean
创造的对象不是布尔值:
let createBoolean: boolean = new Boolean(1);
事实上 new Boolean()
返回的是一个 Boolean
对象:
let createBoolean: Boolean = new Boolean(1);
直接调用 Boolean
也可以返回一个 boolean 类型:
let createBoolean: boolean = Boolean(1);
在 TypeScript 中,boolean
是 JavaScript 中的基本类型,而 Boolean
是 JavaScript 中的构造函数。其他基本类型(除了 null 和 undefined)一样
使用 number
定义数值类型,除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量:
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
使用 string
定义字符串类型:
let myName: string = 'Tom';
let myAge: number = 25;
let sentence: string = `Hello, my name is ${ myName }.
空值Void
JavaScript 没有空值(Void
)的概念,在 TypeScript 中,可以用 void
表示没有任何返回值的函数:
function alertName(): void {
alert('My name is Tom');
声明一个void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
:
let unusable: void = undefined;
Null 和 Undefined
TypeScript里,undefined
和null
两者各自有自己的类型分别叫做undefined
和null
。 和 void相似,它们的本身的类型用处不是很大。与 void
的区别是,默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
类型的变量。
let num: number = undefined;
let u: undefined;
let num: number = u;
let u: void;
let num: number = u;
在对现有代码进行改写的时候,any
类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let myNumber: string = 'seven';
myNumber = 7;
但如果是 any
类型,则允许被赋值为任意类型。
let myNumber: any = 'seven';
myNumber = 7;
在任意值上访问任何属性都是允许的:
let anyThing: any = 'hello';
console.log(anyThing.name);
console.log(anyThing.name.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let anything;
anything = 'seven';
anything = 7;
anything.setName('Tom');
let anything: any;
anything = 'seven';
anything = 7;
anything.setName('Tom');
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
以下代码虽然没有指定类型,但是会在编译的时候报错:
let myFavoriteNumber = 'eight';
myFavoriteNumber = 8;
事实上,它等价于:
let myFavoriteNumber: string = 'eight';
myFavoriteNumber = 8;
TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:
let myFavoriteNumber;
myFavoriteNumber = 'eight';
myFavoriteNumber = 8;
联合类型(Union Types)表示取值可以为多种类型中的一种。
联合类型使用 |
分隔每个类型。
let myFavoriteNumber: string | number;
myFavoriteNumber = 'eight';
myFavoriteNumber = 8;
这里的 let myFavoriteNumber: string | number
的含义是,允许 myFavoriteNumber
的类型是 string
或者 number
,但是不能是其他类型。
访问联合类型的属性或方法
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {
return something.length;
length
不是 string
和 number
的共有属性,所以会报错。
访问 string
和 number
的共有属性是没问题的:
function getString(something: string | number): string {
return something.toString();
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
let myFavoriteNumber: string | number;
myFavoriteNumber = 'eight';
console.log(myFavoriteNumber.length);
myFavoriteNumber = 8;
console.log(myFavoriteNumber.length);
上例中,第二行的 myFavoriteNumber
被推断成了 string
,访问它的 length
属性不会报错。
而第四行的 myFavoriteNumber
被推断成了 number
,访问它的 length
属性时就报错了。
对象的类型——接口
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
简单的例子
interface Animal {
name: string;
age: number;
let tom: Animal = {
name: 'nick',
age: 4,
上面的例子中,我们定义了一个接口 Animal
,接着定义了一个变量 nick
,它的类型是 Animal
。这样,我们就约束了 nick
的形状必须和接口 Animal
一致。
接口一般首字母大写。有的编程语言中会建议接口的名称加上 I 前缀。
定义的变量比接口少了一些属性是不允许的:
interface Person {
name: string;
age: number;
let tom: Person = {
name: 'nick'
多一些属性也是不允许的:
interface Person {
name: string;
age: number;
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
可选属性的含义是该属性可以不存在。用 ?:
符号来标记可选属性:
interface Person {
name: string;
age?: number;
let tom: Person = {
name: 'Tom'
这时仍然不允许添加未定义的属性:
interface Person {
name: string;
age?: number;
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
interface Person {
name: string;
age?: number;
[propName: string]: any;
let tom: Person = {
name: 'Tom',
gender: 'male'
使用 [propName: string]
定义了任意属性取 string
类型的值。
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
interface Person {
name: string;
age?: number;
[propName: string]: string;
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
上例中,任意属性的值允许是 string
,但是可选属性 age
的值却是 number
,number
不是 string
的子属性,所以报错了。一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {
name: string;
age?: number;
[propName: string]: string | number;
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly
定义只读属性:
interface House {
readonly id: number;
name: string;
[propName: string]: any;
let house: House = {
id: 89757,
name: '福克斯',
address: '中国'
house.id = 9527;
上例中,使用 readonly
定义的属性 id
初始化后,又被赋值了,所以报错了。
注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
interface House {
readonly id: number;
name: string;
[propName: string]: any;
let house: House = {
name: '福克斯',
address: '中国'
house.id = 89757;
上例中,报错信息有两处,第一处是在对 house
进行赋值的时候,没有给 id
赋值。
第二处是在给 house.id
赋值的时候,由于它是只读属性,所以报错了。
函数的类型
一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:
function sum(x: number, y: number): number {
return x + y;
注意,输入多余的(或者少于要求的)参数,是不被允许的:
function sum(x: number, y: number): number {
return x + y;
sum(1, 2, 3);
sum(1);
函数表达式
如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:
let mySum = function (x: number, y: number): number {
return x + y;
这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum
,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum
添加类型,则应该是这样:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
用接口定义函数的形状
我们也可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc {
(source: string, subString: string): boolean;
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
采用函数表达式或者接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
与接口中的可选属性类似,我们用 ?
表示可选的参数:
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
let kobebrant = buildName('kobe', 'brant');
let eric = buildName('eric');
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
function buildName(firstName: string, lastName: string = 'lastName') {
return firstName + ' ' + lastName;
let kobebrant = buildName('kobe', 'brant');
let eric = buildName('eric');
此时就不受「可选参数必须接在必需参数后面」的限制了.
ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
let a: any[] = [];
push(a, 1, 2, 3);
事实上,items 是一个数组。所以我们可以用数组的类型来定义它:
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
let a = [];
push(a, 1, 2, 3);
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
比如,我们需要实现一个函数 reverse
,输入数字 123
的时候,输出反转的数字 321
,输入字符串 'hello'
的时候,输出反转的字符串 'olleh'
。
利用联合类型,我们可以这么实现:
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
这时,我们可以使用重载定义多个 reverse
的函数类型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
上例中,我们重复定义了多次函数 reverse
,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
类型断言(Type Assertion)可以用来手动指定一个值的类型。
值 as 类型
建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法
类型断言的用途
将一个联合类型断言为其中一个类型
之前提到过,当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法:
interface Cat {
name: string;
run(): void;
interface Fish {
name: string;
swim(): void;
function getName(animal: Cat | Fish) {
return animal.name;
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如:
interface Cat {
name: string;
run(): void;
interface Fish {
name: string;
swim(): void;
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
return false;
上面的例子中,获取 animal.swim
的时候会报错。
此时可以使用类型断言,将 animal
断言成 Fish
:
interface Cat {
name: string;
run(): void;
interface Fish {
name: string;
swim(): void;
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
return false;
这样就可以解决访问 animal.swim
时报错的问题了。
需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
interface Cat {
name: string;
run(): void;
interface Fish {
name: string;
swim(): void;
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
swim(tom);
将一个父类断言为更加具体的子类
当类之间有继承关系时,类型断言也是很常见的:
class ApiError extends Error {
code: number = 0;
class HttpError extends Error {
statusCode: number = 200;
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
return false;
上面的例子中,我们声明了函数 isApiError
,它用来判断传入的参数是不是 ApiError
类型,为了实现这样一个函数,它的参数的类型肯定得是比较抽象的父类 Error
,这样的话这个函数就能接受 Error
或它的子类作为参数了。
但是由于父类 Error
中没有 code
属性,故直接获取 error.code
会报错,需要使用类型断言获取 (error as ApiError).code
。在这个例子中有一个更合适的方式来判断是不是 ApiError
,那就是使用 instanceof
:
class ApiError extends Error {
code: number = 0;
class HttpError extends Error {
statusCode: number = 200;
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
return false;
上面的例子中,确实使用 instanceof
更加合适,因为 ApiError
是一个 JavaScript 的类,能够通过 instanceof
来判断 error
是否是它的实例。
但是有的情况下 ApiError
和 HttpError
不是一个真正的类,而只是一个 TypeScript 的接口(interface
),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof
来做运行时判断了:
interface ApiError extends Error {
code: number;
interface HttpError extends Error {
statusCode: number;
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
return false;
将任何一个类型断言为 any
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
const foo: number = 1;
foo.length = 1;
上面的例子中,数字类型的变量 foo
上是没有 length
属性的,故 TypeScript 给出了相应的错误提示。
这种错误提示显然是非常有用的。
但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
window.foo = 1;
上面的例子中,我们需要将 window
上添加一个属性 foo
,但 TypeScript 编译时会报错,提示我们 window
上不存在 foo
属性。
此时我们可以使用 as any
临时将 window
断言为 any
类型:
(window as any).foo = 1;
在 any
类型的变量上,访问任何属性都是允许的。
需要注意的是,将一个变量断言为 any
可以说是解决 TypeScript 中类型问题的最后一个手段。
它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any
.
将 any
断言为一个具体的类型
在日常的开发中,我们不可避免的需要处理 any
类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。
遇到 any
类型的变量时,我们可以选择无视它,任由它滋生更多的 any
。
我们也可以选择改进它,通过类型断言及时的把 any
断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。
举例来说,历史遗留的代码中有个 getCacheData
,它的返回值是 any
:
function getCacheData(key: string): any {
return (window as any).cache[key];
那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作:
function getCacheData(key: string): any {
return (window as any).cache[key];
interface Cat {
name: string;
run(): void;
const nick = getCacheData('nick') as Cat;
nick.run();
上面的例子中,我们调用完 getCacheData
之后,立即将它断言为 Cat
类型。这样的话明确了 nick
的类型,后续对 nick
的访问时就有了代码补全,提高了代码的可维护性。
任何类型都可以被断言为 any
any
可以被断言为任何类型
那么是不是可以使用双重断言 as any as Foo
来将任何一个类型断言为任何另一个类型呢?
interface Cat {
run(): void;
interface Fish {
swim(): void;
function testCat(cat: Cat) {
return (cat as any as Fish);
在上面的例子中,若直接使用 cat as Fish 肯定会报错,因为 Cat 和 Fish 互相都不兼容。但是若使用双重断言,则可以打破.若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
类型断言 vs 类型转换
类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {
return something as boolean;
toBoolean(1);
在编译后会变成:
function toBoolean(something) {
return something;
toBoolean(1);
所以类型断言不是类型转换,它不会真的影响到变量的类型。
现在需要一个函数,我们输入什么它就返回什么。不用泛型的话,这个函数是这样的:
function justReturn(arg: number): number {
return arg;
或者,我们使用any类型来定义函数:
function justReturn(arg: any ): any {
return arg;
使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 我们给justReturn
添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
function justReturn<T>(arg: T): T {
return arg;
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output = justReturn<string>("myString");
这里我们明确的指定了T是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = justReturn("myString");
使用泛型变量
使用泛型创建像justReturn
这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。
function justReturn<T>(arg: T): T {
return arg;
如果我想同时打印下arg
的长度, 我们很可能会这样做:
function justReturn<T>(arg: T): T {
console.log(arg.length);
return arg;
编译器会报错说我们使用了arg
的.length
属性,但是没有地方指明arg
具有这个属性。 这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length
属性的。
现在假设我们想操作T
类型的数组而不直接是T
。由于我们操作的是数组,所以.length
属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:
function justReturn<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
我们也可以这样实现上面的例子:
function justReturn<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
还是之前的例子:
function justReturn<T>(arg: T): T {
console.log(arg.length);
return arg;
除了将T改成数组类型,还可以对泛型进行约束,只允许这个函数传入那些包含 length
属性的变量。这就是泛型约束:
interface Lengthwise {
length: number;
function justReturn<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
上例中,我们使用了 extends
约束了泛型 T
必须符合接口 Lengthwise
的形状,也就是必须包含 length
属性。
之前了解过,可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc {
(source: string, subString: string): boolean;
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
当然也可以使用含有泛型的接口来定义函数的形状:
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
return result;
createArray(3, 'x');
与泛型接口类似,泛型也可以用于类的类型定义中:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
复制代码