搭建 TypeScript 环境 & TSC 命令的使用 & 配置 tsconfig 文件
如何进阶TypeScript功底?一文带你理解TS中各种高级语法
TypeScript 是安德斯·海尔斯伯格(C#的首席架构师)于 2012 年 10 月发布。
TS 的特点:
TypeScript 编译器:tsc。
【注意】目前大多数浏览器不支持 TS,需要编译成 JS 后,才能在浏览器上运行。
JavaScript 的类型分为两种:
TypeScript 数据类型有 14 种:
表示逻辑值:true 和 false。
let isDone: boolean = false;
支持二进制、八进制、十进制和十六进制字面量。
let decLiteral: number = 6;
【注意】TypeScript 和 JavaScript 都没有整数类型,都是浮点数。
可以使用 双引号、单引号 或 模版字符串 来表示字符串。
let name: string = "bob";
ts 中的 null 类型 和 undefined 类型的特点:
let un: undefined = undefined
let nu: null = null
// un = 'a' // 报错:不能将类型“"a"”分配给类型“undefined”。
// nu = 1 // 报错:不能将类型“"1"”分配给类型“null”。
// 可以将其他类型的值赋值给 undefined 或 null 类型的变量
let test: number | undefined | null = 1
test = undefined
test = null
【注意】
当你指定了 --strictNullChecks
标记,null 和 undefined 只能赋值给 void 和 它们自身。
--strictNullChecks
标记:等价于在 tsconfig.json 文件中的 compilerOptions 对象里设置了 strict: 'true'
,从而开启了严格模式。
在变量后使用“ ! ”即可:变量名!
。
例如:
export interface DataConfig {
url?: string
params?: any
type?: string
}
function transformData(config: DataConfig): string {
const { url, params } = config1⃣️
return buildUrl(url!, params)
}
Symbol 是 ES2015 新增类型,它的功能类似于一种唯一标识的 ID。
symbol 类型的特点:
let s1: symbol = Symbol() // 显示声明一个symbol
let s2 = Symbol()
console.log('比较symbol的变量:', s1 === s2); // false。因为 symbol 是唯一的。
–> 像字符串一样,symbols 可以被用做——对象属性的键。
let sym = Symbol();
let obj = {
[sym]: "value"
};
console.log(obj[sym]); // "value"
–> Symbols 也可以与 计算出的属性名声明 相结合来——声明对象的属性 和 类成员。
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol](){
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
属性 | 描述 |
---|---|
Symbol.hasInstance | 方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。 |
Symbol.isConcatSpreadable | 布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。 |
Symbol.iterator | 方法,被for-of语句调用。返回对象的默认迭代器。 |
Symbol.match | 方法,被String.prototype.match调用。正则表达式用来匹配字符串。 |
Symbol.replace | 方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。 |
Symbol.search | 方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。 |
Symbol.species | 函数值,为一个构造函数。用来创建派生对象。 |
Symbol.split | 方法,被String.prototype.split调用。正则表达式来用分割字符串。 |
Symbol.toPrimitive | 方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。 |
Symbol.toStringTag | 方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。 |
Symbol.unscopables | 对象,它自己拥有的属性会被with作用域排除在外。 |
BigInt 是ES2020新增数据类型,用于支持比Number数据类型支持的范围更大的整数值。使用BigInt,整数溢出的问题将不复存在。
// bigint 数值,需要在数字后面加字母n
const bigint1: bigint = 999999999999999999n
// 也可以使用 bigint 构造函数来表示
const bigint2: bigint = BigInt('9999999999999')
在 ts 中,如果我们不指定一个变量的类型,那么默认这个变量的类型是any类型。我们可以任意给它赋值。所以,如果不是特殊情况,我们不建议将变量定义为 any 类型。否则,就没必要使用 ts 了。
any 表示:任意类型。
例如:
let notSure: any = 4;
notSure = "qwert";
notSure = true;
当一个数组中要存储多个数据,可数据的 个数 或 类型 不确定时,就需要使用 any 类型来定义数组。不过,其弊端是,有时候在代码中不会有类型的错误提示信息(代码编译竟然能通过),幸亏在浏览器上会正常抛出此错误。
let arr: any[] = [123, 'qwert', true]
// 若像上面这样定一个不确定个数和类型的数组,
console.log(arr[0].split('')); // 数字类型的数据是没有split方法的,所以此处应该报错,但是代码编译通过了,幸亏浏览器能正常捕获此错误。
在 js 中,void 是一种操作符,它可以让任表达式返回 undefined,比如:返回 undefined 最便捷的一个方法是执行代码——
void 0;
。
在 ts 中,void 表示:没有任何返回值的类型。比如:一个没有任何返回值的函数,它的类型就是 void 类型。
void 类型与 any 类型相反,它表示:没有任何类型。
void 的使用:
当一个函数没有返回值时,可以指定其返回值类型为 void。
例如:
function showMsg(): void {
//
}
console.log(showMsg()); // undefined
在指定了一个函数的返回值类型为 void 后,若该函数体中使用了 return
关键字返回了任意类型的值,都可以正常返回。
function showMsg(): void {
return 123
}
console.log(showMsg()); // 123
可以定义一个 void 类型的变量,不过意义不大,因为它只能等于 undefined 或 null。
例如:
let vd: void = undefined;
let vd: void = null;
nerver 类型表示:永远不会有返回值的类型。
nerver 类型的特点:
有两种情况 永远不会有返回值的类型:
// 1、一个函数抛出了异常,那么这个函数永远不会有返回值,那么它的类型就是 never。
let error = () => {
throw new Error('error')
}
// 2、死循环函数永远不会有返回值,那么它的类型也是 never。
let endless = () => {
while(true) {}
}
定义一个 TypeScript 数组的两种方式:
: 数据类型[]
定义一个数组。: Array<数据类型>
定义一个数组。在 TypeScript 里,数组定义后,里面的数据的类型必须和定义数组的时候的类型一致,否则会有错误提示,编译失败。
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3]; // 数组泛型
【注意】
在 TypeScript 里,若要存储不同类型的数据,可以但不建议使用数组(建议用元组):
// 可以但不建议使用数组存储不同类型的数据
const arr: (string | number | boolean)[] = [1, '2', true];
元组本身就是一个数组,它更像是 JavaScript 里的数组,与 typescript 里的数组不同的是:各元素的类型不必相同。
在使用元组类型时,数据的 类型、个数 和 位置,必须与定义元组时限定的数据的 类型、个数 和 位置 一一对应
// 声明一个元组类型
let x: [string, number] = ['qwer', 123, true];
建议: 数组(Array)用于保存相同类型元素集合,元组(Tuple)用于保存不同类型元素集合。
【拓展】元组越界问题
let tuple: [number, string] = [1, 'a']
tuple.push(3)
console.log('为元组push一个新成员: ', tuple);
// [1, 'a', 3] 这歌结果说明:ts允许我们往元组中push新的元素,但是访问呢?
console.log('访问push进元组的数据:', tuple1[2]);
// 报错提示:“长度为 "2" 的元组类型 "[number, string]" 在索引 "2" 处没有元素”。由此可见,无法访问。
由上述代码推论可知:我们可以通过 push 方法为元组添加新元素,但是仍然不能进行越界访问。实际开发中强烈不建议这样使用元组。
object 是包含一组键值对的实例。
// 直接创建一个对象
const obj = {};
// 使用接口(Interfaces)来定义对象
interface Person {
name : string ,
age: number
}
【注意】
与 js 不同,在 ts 中,不允许通过 “对象.属性名
” 的方式直接修改对象里属性的值。
let obj: object = { x: 1, y: 2 }
因为我们只是简单的定义 obj 是一个 object,但并未定义 obj 应该包含的属性的数据类型。所以,正确的方法如下:
let obj2: {x: number, y: number} = { x: 1, y: 2 }
obj2.x = 3
function getObj (obj: object): object {
return {}
}
TypeScript 中的枚举是:一组有名字的常量集合。
定义枚举类型的语法:
enum 枚举名 {
成员一,
成员二 = 成员二的值 // 成员二的值包括:数字(常量 或 计算)和 字符串
};
枚举的特性:
const
声明的枚举是常量枚举。declare
声明的枚举是外部枚举。+, -, ~
。例如: -1, -100。 +, -, *, /, %, <<, >>, >>>, &, |, ^
。例如:1 + 3。例如:
// 定义一个枚举
enum Char {
// 常量成员
a, // 默认是 0
b, // 默认是 1
c = 100,
d = Char.a + 3,
// 计算成员
e = Math.random(),
f = 'qwer'.length,
g = 1, // 紧跟在计算成员后的第一个常量成员,必须手动赋值,否则会报错。
};
例如:
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
【拓展】
例如:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
other = "日月"
}
用 const
声明的枚举是常量枚举。
const enum Enum {
A = 1,
B = A * 2
}
用 declare
声明的枚举是外部枚举。
declare enum Enum {
A = 1,
B,
C = 2
}
【注意】以 declare 声明的变量和模块后,其他地方不需要引入,就可以直接使用了。
枚举的实现原理是:双向映射。
例如:有这样一个枚举
enum Direction {
Up,
Down,
Left,
Right
}
将其编译为 js:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
可见,它先以键取值(正向映射),又以值取键(反向映射)。
不过,字符串枚举 是不能反向映射的。
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
可以用 type
关键字来定义字符串字面量类型。
例如:
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}
handleEvent(document.getElementById('hello'), 'scroll'); // 没问题
handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'
// index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.
上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。
在 ts 中,在有些没有明确指出类型的地方,tsc 类型推断就会帮助我们提供类型。
let x1 = 3//此时,x1 变量的类型就会默认推断为 number。
这种推断,除了发生在变量中,还可以发生在初始化变量的时候,或者是设置默认参数、决定参数返回值的时候。
最佳通用类型:有时我们需要从几个表达式中推断类型,那么我们会使用这些表达式类型来推断出一个最合适的通用类型。
例如:
let x2 = [0, 1, null]
为了推断 x2 的类型,必须考虑所有元素的类型,这里我们两种选择,一个是 number,一个是 null。推断通用类型就会考虑到所有的候选类型,然后给出一个兼容所有候选类型的一个类型。x2 的候选类型是 number 和 null 这样的一个联合类型。
最终的通用类型取自于候选类型。有些时候,候选类型是共享一个公共结构的,但他们却没有一个类型能作为所有候选类的超级类型。
例如:
class Animal {
numLegs: number
}
class Bee extends Animal {}
class Lion extends Animal {}
let zoo = [new Bee(), new Lion()]
这里,我们想要 zoo 被推断为一个 Animal 数组类型。但是这个数组中没有一个对象明确是 Animal 类型的。因此,它是不能被推断出这个结果的。此时,zoo 找不到最佳的通用类型,就会被推断为 Bee 和 Lion 的一个联合数组类型。所以,有些时候,我们为了更正它,我们可以明确声明我们期望的类型。比如:
let zoo2: Animal[] = [new Bee(), new Lion()]
有时候 TS 的类型推断会按照另外一种方式进行——上下文类型。
上下文类型的出现,和表达式的类型,以及它所处的位置是相关的。
比如:
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.clickTime);
}
这个列子,我们会得到一个错误:clickTime is undefined。
这是因为:TS 类型检查器,会使用 window.onmousedown 的函数类型,来推断右边函数表达式的类型。那么我们就可以推断出这个参数的类型。但是,mouseEvent 是 event 类型,它显然是没有 clickTime 属性的,访问了一个不存在的属性,所以会报 undefined。我们可以通过给 上下文表达式 指定一个明确的类型,来解决这个问题。
如果上下文表达式包含明确的类型解析,那么这个上下文类型就会被忽略。
比如:
window.onmousedown = function (mouseEvent: any) {
console.log(mouseEvent.clickTime);
}
这样明确指明类型为 any 后,这个上下文类型就被忽略了,就不报错了。
上下文类型是一个非常有用的一个类型推断,它在很多情况下都会用到,通常包括函数的参数、赋值表达式右边、类型断言、对象成员、函数的 字面量 和 返回值语句。
上下文类型也可以作为 最佳通用类型的一个候选类型。
例如:
class Animal2 {
numLegs: number
}
class Bee2 extends Animal2 {}
class Lion2 extends Animal2 {}
function createZoo (): Animal2[] {
return [new Bee2(), new Lion2()];
}
此时,最佳通用类型有 3 个候选类型: Animal2、Bee2 和 Lion2。这里 Animal2 就会作为最佳通用类型。这也是上下文类型的一个应用。
“类型断言”:可以用来手动指定一个值的类型。
类型断言 不是 强制类型转换。
语法:值 as 类型
或 <类型>值
。
例如:
let someValue: any = "this is a string";
let strLength1: number = (<string>someValue).length;
let strLength2: number = (someValue as string).length;
【注意】:在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者——故建议大家统一使用 as 的语法做类型断言。
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;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
上面的例子中,获取 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);
// Uncaught TypeError: animal.swim is not a function`
上面的例子编译时不会报错,但在运行时会报错。原因是 (animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。
当类之间有继承关系时,类型断言也是很常见的:
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 (error instanceof ApiError) {
return true;
}
return false;
}
// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
此时就只能用类型断言,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiError 了:
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;
}
理想情况下,TypeScript 的类型系统运转良好,每个值的类型都具体而精确。
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
const foo: number = 1;
foo.length = 1;
// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.
上面的例子中,数字类型的变量 foo 上是没有 length 属性的,故 TypeScript 给出了相应的错误提示。
这种错误提示显然是非常有用的。
但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
上面的例子中,我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。
此时我们可以使用 as any 临时将 window 断言为 any 类型:
(window as any).foo = 1;
在 any 类型的变量上,访问任何属性都是允许的。
需要注意的是,将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。
它极有可能掩盖了真正的类型错误,所以:如果不是非常确定,就不要使用 as 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 tom = getCacheData('tom') as Cat;
tom.run();
上面的例子中,我们调用完 getCacheData 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。
并不是任何一个类型都可以被断言为任何另一个类型——具体来说,若 A 兼容 B,那么 A 能够被断言为 B,B 也能被断言为 A。
例如:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
let tom: Cat = {
name: 'Tom',
run: () => { console.log('run') }
};
let animal: Animal = tom;
在上述案例中,Cat 包含了 Animal 中的所有属性,除此之外,它还有一个额外的方法 run。由于 TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。 所以,TypeScript 并不关心 Cat 和 Animal 之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 Cat extends Animal 是等价的:
interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}
那么也不难理解为什么 Cat 类型的 tom 可以赋值给 Animal 类型的 animal 了——就像面向对象编程中我们可以将子类的实例赋值给类型为父类的变量。
我们把它换成 TypeScript 中更专业的说法,即:Animal 兼容 Cat。
当 Animal 兼容 Cat 时,它们就可以互相进行类型断言了:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
function testAnimal(animal: Animal) {
return (animal as Cat);
}
function testCat(cat: Cat) {
return (cat as Animal);
}
这样的设计其实也很容易就能理解:
使用双重断言可以将任何一个类型断言为任何另一个类型。
比如:
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish);
}
在上面的例子中,若直接使用 cat as Fish 肯定会报错,因为 Cat 和 Fish 互相都不兼容。
但是若使用双重断言,则可以打破「要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可」的限制,将任何一个类型断言为任何另一个类型。
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。除非迫不得已,千万别用双重断言。
类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
// 返回值为 1
在上面的例子中,将 something 断言为 boolean 虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
function toBoolean(something) {
return something;
}
toBoolean(1);
// 返回值为 1
所以类型断言不是类型转换,它不会真的影响到变量的类型。
若要进行类型转换,需要直接调用类型转换的方法:
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
// 返回值为 true
在这个例子中:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
我们使用 as Cat 将 any 类型断言为了 Cat 类型。
但实际上还有其他方式可以解决这个问题:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom: Cat = getCacheData('tom');
tom.run();
上面的例子中,我们通过类型声明的方式,将 tom 声明为 Cat,然后再将 any 类型的 getCacheData(‘tom’) 赋值给 Cat 类型的 tom。
这和类型断言是非常相似的,而且产生的结果也几乎是一样的——tom 在接下来的代码中都变成了 Cat 类型。
它们的区别,可以通过这个例子来理解:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom = animal as Cat;
在上面的例子中,由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom。
但是若直接声明 tom 为 Cat 类型:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom: Cat = animal;
// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
则会报错,不允许将 animal 赋值为 Cat 类型的 tom。
这很容易理解,Animal 可以看作是 Cat 的父类,当然不能将父类的实例赋值给类型为子类的变量。
深入的讲,它们的核心区别就在于:
animal 断言为 Cat,只需要满足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可
animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行
但是 Cat 并不兼容 Animal。
而在前一个例子中,由于 getCacheData(‘tom’) 是 any 类型,any 兼容 Cat,Cat 也兼容 any,故
const tom = getCacheData('tom') as Cat;
//等价于
const tom: Cat = getCacheData('tom');
知道了它们的核心区别,就知道了:类型声明是比类型断言更加严格的。
所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。
还是这个例子:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
我们还有第三种方式可以解决这个问题,那就是泛型:
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();
通过给 getCacheData 函数添加了一个泛型 ,我们可以更加规范的实现对 getCacheData 返回值的约束,这也同时去除掉了代码中的 any,是最优的一个解决方案。
将多个类型合并成一个类型。将现有的多种类型叠加到一起,合并成一个新的类型。大多数是在其他 混入 或者 不适合典型面向对象模型 的地方可以看到交叉类型的使用。
使用 &
管道符 可以定义 交叉类型:
let val: string & number
let arr: number[] & string[]
问题再现:
interface T{}
interface U{}
function extend<T, U>(first: T, second: U): T & U {
let result = {} as T & U
for(let id in first) {
// result[id] = first[id]
//报错了:不能将类型“T[Extract]”分配给类型“(T & U)[Extract]”。
// 不能将类型“T”分配给类型“T & U”。
// 不能将类型“T”分配给类型“U”。
// “U”可以使用与“T”无关的任意类型进行实例化。
}
for(let id in second) {
if(!result.hasOwnProperty(id)){
// result[id] = second[id]
//报错了:不能将类型“U[Extract]”分配给类型“(T & U)[Extract]”。
// 不能将类型“U”分配给类型“T & U”。
// 不能将类型“U”分配给类型“T”。
// “T”可以使用与“U”无关的任意类型进行实例化。
}
}
return result
}
上述案例中,“T & U”这个类型就是交叉类型。
问题的解决:
此时,为了让这段代码编译通过,我们可以在复制的时候 给它指定 一个明确的 any 类型。
function extend2<T, U>(first: T, second: U): T & U {
let result = {} as T & U
for(let id in first) {
result[id] = first[id] as any
}
for(let id in second) {
if(!result.hasOwnProperty(id)){
result[id] = second[id] as any
}
}
return result
}
这样编译就通过了。
接下来,我们去使用这个 extend2 函数
class Person {
constructor(public name: string) {}
}
interface Loggable {
log(): void
}
class ConsoleLogger implements Loggable {
log() {}
}
var jim = extend(new Person('jim'), new ConsoleLogger())
jim.name
jim.log()
这里,把 Person 实例和 ConsoleLogger 实例通过 extend 扩展到一起,使他成为一个交叉类型。那么,jim 实际上就是 Person 类和 ConsoleLogger 类返回结果的一个交叉。所以,jim 既可以访问 name 属性,又可以调 log 方法。
使用 |
管道符 可以定义 联合类型:
let val: string | number
let arr: number[] | string[]
|
间隔多个类型。&
间隔多个类型。/**
* @param value string
* @param padding number | string
*/
function padLeft(value: string, padding: any) {
if(typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if(typeof padding === 'string') {
return padding + value
}
throw new Error(`${padding}异常`)
}
padLeft('hello world', 4)
padLeft('hello world', true)
其实上述 padLeft 函数存在一个问题,因为 padding 的类型是 any,所以,实际上可以传入的值可以既不是number也不是string类型的其他类型的值。此时,ts 不会报错,只有在执行 padLeft 函数后才会抛出 new Error 里定义的错误。那么如何在书写代码时,让 ts 推断出类型的错误呢?可以使用联合类型。
function padLeft2(value: string, padding: string | number) {
if(typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if(typeof padding === 'string') {
return padding + value
}
throw new Error(`${padding}异常`)
}
padLeft2('hello world', 4)
// padLeft2('hello world', true)
//报错了:类型“boolean”的参数不能赋给类型“string | number”的参数。
如果一个值是联合类型,那么我们只能访问这个联合类型的所有类型的共有成员。
比如:
interface Bird {
fly()
layEggs()
}
interface Fish {
swim()
layEggs()
}
function getSmallPet(): Fish | Bird {
//为了将问题简明的表达出来,故此处请忽略 没有返回值 的报错提示
return {} as Fish | Bird
}
let pet = getSmallPet()
pet.layEggs()
// pet.swim()
//报错了:类型“Bird | Fish”上不存在属性“swim”。类型“Bird”上不存在属性“swim”。
这个报错是因为:swim 不是联合类型 “Fish | Bird” 的共有成员,所以会报错。而 layEggs 是联合类型 “Fish | Bird” 的共有成员,就不会报错。
举个例子:
interface Bird2 {
fly()
layEggs()
}
interface Fish2 {
swim()
layEggs()
}
function getSmallPet2(): Fish2 | Bird2 {
//为了将问题简明的表达出来,故此处请忽略 没有返回值 的报错提示
return {} as Fish | Bird
}
let pet2 = getSmallPet2()
在上述案例中,如果想要访问 swim,在 js 中,区分多个可能值的方法是检测成员是否存在:
if(pet2.swim) {
pet2.swim()
}else if(pet2.fly){
pet2.fly()
}
但是在 ts 里,我们每次访问这个成员的时候都会报错,为了让这段代码工作,我们可以使用类型断言将 pet2 强行断言成我们想要的类型。
if((pet2 as Fish2).swim) {
(pet2 as Fish2).swim()
}else if((pet2 as Bird2).fly){
(pet2 as Bird2).fly()
}
可见,通过断言是可以的,但是我们需要多次使用断言。那么有没有更简洁优雅的方式呢?ts 提供了 类型保护机制。
TS 提供的类型保护机制包括:
is
认识类型谓词:
function isFish(pet: Fish2 | Bird2):pet is Fish2 {
return (pet as Fish2).swim !== undefined
}
上述代码中,isFish 函数的返回值类型 “pet is Fish2” 的 is
就是类型谓词。
改写类型保护的案例:
if(isFish(pet2)) {
pet2.swim()//一旦使用了类型谓词,ts会默认推断出 isFish 匹配的类型
} else {
pet2.fly()//一旦使用了类型谓词,ts会默认推断出剩余的类型的属性
}
typeof
在研究 typeof 之前,我们先用 类型谓词 改写下 padLeft:
function isNumber (x: any): x is number {
return typeof x === 'number'
}
function isString (x: any): x is string {
return typeof x === 'string'
}
function padLeft3(value: string, padding: string | number) {
if(isNumber(padding)) {
return Array(padding + 1).join(' ') + value
}
if(isString(padding)) {
return padding + value
}
throw new Error(`${padding}异常`)
}
这样改写是可以的,但是,这里每个类型都去定义一个函数去它是否是指定的一个原始类型,其实判断“原始类型”时,我们没必要去额外定义函数,ts 提供了直接使用 typeof 来做类型保护的方式:
function padLeft4(value: string, padding: string | number) {
if(typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if(typeof padding === 'string') {
return padding + value
}
throw new Error(`${padding}异常`)
}
建议:typeof
可以对 “原始类型” 做 类型保护,非原始类型可以使用 谓词 做类型保护。
instanceof
instanceof 类型保护 是通过构造函数来细化类型的一种方式。用于判断一个对象是不是某一个类的实例对象。instanceof 的右边必须是一个构造函数。
例如:
class Birds {
fly() {
console.log('birds fly');
}
layEggs() {
console.log('birds layEggs');
}
}
class Fishs {
swim() {
console.log('Fishs swim');
}
layEggs() {
console.log('Fishs layEggs');
}
}
function getRandomPet(): Fishs | Birds {
return Math.random() > 0.5 ? new Birds() : new Fishs()
}
let pets = getRandomPet()
//判断pets是不是Birds的实例对象,如果中间有其他值覆盖了,会出现问题
if(pets instanceof Birds) {
pets.fly()
}
//判断pets是不是Fishs的实例对象,如果中间有其他值覆盖了,会出现问题
if(pets instanceof Fishs) {
pets.swim()
}
ts 中具有两种特殊类型:null 和 undefined,他们分别具有值 null 和 undefied。也就是说,null 和 undefined 既可以作为类型也可以作为值。
默认情况下 null 和 undefined 是可以赋值给任何类型的,而且 null 和 undefined 是所有其他类型的一个有效值。
比如:
let s = 'f00'
// s = null
//在 tsc 开启 --strictNullChecks 的编译模式下,会报错:不能将类型“null”分配给类型“string”。
可以用联合类型来解决这个报错:
let sn: string | null = 'bar'
sn = null
// sn = undefined
//在 tsc 开启 --strictNullChecks 的编译模式下,会报错:不能将类型“undefined”分配给类型“string | null”。
这是因为:在严格空值检查模式下,null 和 undefined 无法赋值给其他类型的变量。
【拓展】--strictNullChecks
标记是“开启空值的严格检查”的,平时开发是要开启它的,它能帮助定位并解决一些问题。
function f(x: number, y?: number) {
return x + (y || 0)
}
f(1, 2)
f(1)
f(1, undefined)
//之所以能够传入 undefined,是因为:当 tsc 开启 --strictNullChecks 的时候,可选参数 y 实际上是 number 或者 undefined 的联合类型。
// f(1, null)
//在 tsc 开启 --strictNullChecks 的编译模式下,会报错:类型“null”的参数不能赋给类型“number | undefined”的参数。
//这是因为:在严格空值检查模式下,null 和 undefined 无法赋值给其他类型的变量。
class C {
a: number
b?: number
}
let c = new C()
c.a = 1
c.b = 2
c.b = undefined
//之所以能够传入 undefined,是因为:当 tsc 开启 --strictNullChecks 的时候,可选属性 b 实际上是 number 或者 undefined 的联合类型。
// c.b = null
//在 tsc 开启 --strictNullChecks 的编译模式下,会报错:不能将类型“null”分配给类型“number | undefined”。
//这是因为:在严格空值检查模式下,null 和 undefined 无法赋值给其他类型的变量。
由于 null
作为 可选属性 或 可选参数 时,ts 默认会将其推断为一个 null | undefined
的联合类型。而且,只有开启 --strictNullChecks
严格空值检查模式时,ts 在编译时才可以捕获其错误。所以,有的时候 我们需要做 null 类型保护——去除 null。
比如:
function f1(sn: string | null): string {
if(sn === null) {
return 'default'
} else {
return sn
}
}
//还可以简写为
function f2(sn: string | null): string {
return sn || 'default'
}
但是,有些时候编译器是无法去除 null 和 undefined。此时可以“用 类型断言 去手动去除”。
用 类型断言 去手动去除 null 的语法:在变量后添加 !
——表示此变量不为 null。
function f3(sn: string | null): string {
return sn! || 'default'
}
来看一个具体的示例:输入一个名字返回其绰号。
function broken(name: string|null): string {
function postfix(epithet: string) {
// return name.charAt(0) + ". the" + epithet
//在 tsc 开启 --strictNullChecks 的编译模式下,会报错::“name”可能为 “null”。
//这是因为:我们此处使用了一个嵌套函数 postfix,编译器是无法对嵌套函数 postfix 的 null 识别的。
return name!.charAt(0) + ". the" + epithet
}
name = name || 'Bob'
return postfix(name)
}
字符串字面量类型 允许我们指定的 字符串 必须具有确切的值。
在实际应用中,字符串字面量类型 是可以和 联合类型、类型保护 配合着使用的。
通过结合使用它们的特性,可以实现类似于 枚举 类型的字符串。
比如:
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === 'ease-in') {}
else if (easing === 'ease-out') {}
else if (easing === 'ease-in-out') {}
else {}
}
}
let button = new UIElement()
button.animate(0, 0, 'ease-in')
// button.animate(0, 0, 'unease')
//报错了:类型“"unease"”的参数不能赋给类型“Easing”的参数。
定义类型别名的方式:
别名: 原名
语法定义一个类型别名;type
关键字定义 类型别名。通过 别名: 原名
语法可以直接给类型起别名。
例如:
interface Person {
name: string
}
// 给接口 Person 起个别名 P
const P: Person = {
name: 'Tom'
}
例如:
type Name = string; // 给 string 起个别名为 Name
type NameResolver = () => string; // 给 `() => string` 起个别名为 NameResolver
type NameOrResolver = Name | NameResolver; // 给 `Name | NameResolver` 起个别名为 NameOrResolver
// 给 NameOrResolver 起个别名为 n
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
【拓展】 type
关键字除了可以定义 类型别名,还可以定义 一个新的类型,包括:原始值、联合类型、元组以及其它任何你需要手写的类型——比如:字符串字面量类型。
变量,可改其值。常量,不可改其值。
变量使用前必须先声明,我们可以:
常见的声明一个变量的方式:
// 使用 js 语法定义一个变量(ts 兼容所有 js 的语法)
let [变量名]; // 定义一个变量但未实现,默认值是 undefined。
let [变量名] = 值; // 定义并赋值一个变量
// 使用 ts 愈发定义一个变量
let [变量名] : [类型]; // 定义一个变量但未实现,默认值是 undefined。
let [变量名] : [类型] = 值; // 定义并赋值一个变量
例如:
let uname;
let uname = "marry";
let uname: string;
let uname: string = "marry";
【注意】变量不要使用 name 否则会与 DOM 中的全局 window 对象下的 name 属性出现了重名。
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
具体请参阅:这里。
TypeScript 通过 interface
关键字来定义接口。
广义上讲,一切皆对象,所以接口是对对象的状态(属性)和行为(方法)的抽象(描述)——为对象的属性和方法定义约束。
狭义上讲,接口可以用来约束对象、函数、类的结构 和 类型。
接口的唯一一个原则:当类型检查器检查时,只要相应的属性存在且类型也匹配即可,其他皆无所谓。
当不确定一个接口中属性的个数或属性的类型时,就可以定义使用 可索引类型的接口。
// 定义一个接口
interface RouteType {
info: any;
type: string;
}
// 声明一个函数,并对函数的形参进行约束
const setRouteModuleList = (list: RouteType[]) => {
list.forEach(item => {
// ...
});
}
// 按照函数形参的约束来定义实参
let params = [
{
type: 'amy',
info: amyModules
}
]
// 调用这个函数,并传入实参
setRouteModuleList(params)
接口中的属性包括:
参数: 类型
。参数?: 类型
。readonly 参数: 类型
。[剩余参数: 类型]: 数量
,其中表示任意数量的数量是 any
。例如:
interface LabelledValue {
label: string;
color?: string;
readonly x: number;
[propName: string]: any;
}
【拓展】Readonly 与 const 的区别:
通过 ReadonlyArray
(T 表示类型)关键字可以定义一个只读数组。
let a: number[] = [1, 2, 3, 4]
let ro: ReadonlyArray<number> = a
// ro[0] = 100; //报错:类型“readonly number[]”中的索引签名仅允许读取。
// ro.length = 10; //报错:无法为“length”赋值,因为它是只读属性。
// a = ro; //报错:类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"。
a = ro as number[] //利用“类型断言”是可以:将一个只读属性赋值给一个变量的。这相当于,在赋值之前将只对属性 ReadonlyArray 强转成了 number[] 类型了。
当传入的参数是对象字面量时,TS 会自动对其进行“额外的属性检查”。
若有额外的属性传入就会报错,那么怎么解决这个报错呢?
interface SquareConfig2 {
color?: string,
width?: number,
}
function createSquare2 (config: SquareConfig2): Square {
let newSquare = { color: 'white', area: 100 }
if (config.color) {
newSquare.color = config.color
}
if (config.width) {
newSquare.area = config.width * config.width
}
return newSquare
}
//如下,显然我们误将 color 拼错成了 colorrr,此时就会触发 TS 的“额外的属性检查”。
// let mySquare2 = createSquare2({ colorrr: 'black', width: 50 });
我们分别试试不同的解决方案:
let mySquare21 = createSquare2({ colorrr: 'black', width: 50 } as SquareConfig2);
const squareOption = { colorrr: 'black', width: 50 }
let mySquare22 = createSquare2(squareOption);
interface SquareConfig23 {
color?: string,
width?: number,
[propName: string]: any//索引的属性名是一个字符串类,索引的值是任意类型。
}
function createSquare23 (config: SquareConfig23): Square {
let newSquare = { color: 'white', area: 100 }
if (config.color) {
newSquare.color = config.color
}
if (config.width) {
newSquare.area = config.width * config.width
}
return newSquare
}
let mySquare23 = createSquare23({ colorrr: 'black', width: 50 });
接口能够描述带有属性的普通对象,也能够描述一个函数类型。
用接口描述函数的好处是:不必显示的约束函数的形参类型和返回值类型了,TS 可以通过签名自动推断出参数和返回值的类型。
interface SerachFunc {
(source: string, subString: string): boolean,
}
let mySearch: SerachFunc;
mySearch = function (source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1;
}
函数的入参 与 接口中函数签名的参数 的命名可以不一致,只需要保证他们对应参数的 类型一致 就可以了。
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
我们也可以完全不写类型(就变成 js 的函数了),ts也能够自己进行类型推断。
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
}
接口还能够描述:那些能够通过索引得到的一个类型。
TS 支持 2 种索引类型:
以 数字签名的索引 为例:
interface StringArray {
[index: number]: string
//该索引的类型是 数字签名的索引。它表示:当用number来索引StringArray数组的时候,会得到一个string类型的返回值。
}
let myArray: StringArray;
myArray = ['Bob', 'Fred'];
//通过数字索引,来得到一个值
let myStr: string = myArray[0];
**字符串签名 和 数字签名 同时使用时,数字签名索引的返回值必须是 字符串签名索引的返回值 的 子类型。**因为同时使用时,会把数字索引转化为字符串,比如:会把 myArray[0] 中的 0 默认转化为字符串类型的 “0”。
所以,我们必须让两者的返回结果保持一致,他们必须是兼容的。来看下面一个例子。
class Animal {
name: string
}
class Dog extends Animal {
breed: string
}
// interface NotOkay {
// [x: number]: Animal
// [x: string]: Dog
// }
上述代码中,NotOkay 报错:“number”索引类型“Animal”不能分配给“string”索引类型“Dog”。
这是由于:其 数字签名索引的返回值 并不是 字符串签名索引返回值 的 子类型。违背了“字符串签名 和 数字签名 同时使用时,数字签名索引的返回值 必须是 字符串签名索引的返回值 的 子类型。”这一原则。怎么修改呢?将二者的 返回值调换一下 就 ok 了。
interface NotOkay2 {
[x: number]: Dog,
[x: string]: Animal
}
字符串的索引签名实际上是一个自定义模式(dictionary),能够确保所有属性与 “字符串的索引签名” 的返回值的类型匹配。
interface NumberDictionary {
[index: string]: number,
length: number,
// name: string
}
上述接口中,“name: string” 报错了:类型“string”的属性“name”不能赋给“string”索引类型“number”。
这是因为:在定义了 字符串索引签名 后,这就要求:其后的所有属性的返回值的类型必须与 字符串索引的返回值类型 保持一致,否则就会报错。
interface ReadonlyStringArray {
readonly [index: number]: string
}
let myArr: ReadonlyStringArray = ['alice', 'Bob'];
// myArr[2] = 'Mallory';
//报错:类型“ReadonlyStringArray”中的索引签名仅允许读取。
可索引类型具有一个 索引签名,该索引签名可以是 string 或 number类型。它描述了对象索引的类型,还有相应的索引返回值类型。
implements
)在 TS 中,类类型 可以是 基于接口来实现(也叫:类式实现接口)。
类式实现接口的特点:
类类型的实质:用类实现一个接口,然后把当前接口看作是这个类的类型。
可以定义一个类,并通过 implements
关键字来实现一个接口。
interface ClockInterface {
currentTime: Date,
setTime(d: Date)
}
// implements:用来实现一个接口。
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) {}
setTime(d: Date) {
this.currentTime = d
}
}
类实现接口,接口实际上只描述了类的公共部分。不会检查类的私有成员的。
操作类的接口时,类有两种类型:
用 类 实现一个 “构造器的接口” 会报错,比如:
interface ClockConstructor {
currentTime: Date
new(hour: number, minute: number)
setTime(d: Date)
}
// class Clock1 implements ClockConstructor {
// currentTime: Date;
// constructor(h: number, m: number) {}
// setTime(d: Date) {
// this.currentTime = d
// }
// }
上述代码会报错:类“Clock1”错误实现接口“ClockConstructor”。类型“Clock1”提供的内容与签名“new (hour: number, minute: number): any”不匹配。
这是因为:类实现接口时,实际上会对实例部分(比如:currentTime 和 setTime)做类型检查。而构造器存在于类的静态部分,所以是不会做检查的。
类相关的接口包括:类的 实例接口 和 构造器接口。
那么对于类,什么时候该用实例接口?什么时候该用构造器接口呢?
看个例子:
//实例接口
interface ClockInterface2 {
tick()
}
//构造器接口
interface ClockConstructor2 {
new(hour: number, minute: number): ClockInterface2
}
//工厂模式
function createClock(ctor: ClockConstructor2, hour: number, minute: number): ClockInterface2 {
return new ctor(hour, minute)
}
/**定义 类 去 实现 实例接口 */
//实例类1--数字时钟
class DigitalClock implements ClockInterface2 {
constructor(h: number, m: number) {}
tick() {
console.log('beep beep');
}
}
//实例类2--指针时钟
class AnalogClock implements ClockInterface2 {
constructor(h: number, m: number) {}
tick() {
console.log('tick toc');
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
与类一样,接口也是可以继承的。接口可以通过 extends
关键字来 继承 其他的接口。
interface Shap {
color: string
}
interface PenStroke {
penWidth: number
}
//一个接口继承另一个接口
interface Square1 extends Shap {
sideLength: number
}
let square = {} as Square1
square.color = 'blue'
square.sideLength = 10
//一个接口继承多个接口
interface Square2 extends Shap, PenStroke {
sideLength: number
}
let square2 = {} as Square2
square2.color = 'blue'
square2.sideLength = 10
square2.penWidth = 5.0
接口可以用来描述 JS 中丰富的类型,JS 具有动态灵活的特点,有时候我们希望:一个对象中同时具有多种类型。
interface Counter {
(state: number): string //函数
interval: number //对象的属性
reset(): void //对象的方法
}
function getCounter(): Counter {
let counter = (function (star: number) {}) as Counter
counter.interval = 123
counter.reset = function () {}
return counter
}
let c = getCounter()
c(10)
c.reset()
c.interval = 5.0
(见类->接口继承类)
接口继承类指的是:一个 接口 继承一个 类的类型。
接口继承类的特性:
举个例子:
class Control {
private state: any
}
interface SelectableControl extends Control {
select()
}
class button extends Control implements SelectableControl {
select () {}
}
// button 正常
class TextBox extends Control {
select () {}
}
// class ImageC implements SelectableControl {
// select() {}
// }
上述代码中,ImageC 类报错:类“ImageC”错误实现接口“SelectableControl”。类型 “ImageC” 中缺少属性 “state”,但类型 “SelectableControl” 中需要该属性。
这是因为:ImageC 并没有继承 Control,它去实现 SelectableControl 的时候是缺少 state 属性的。接口去继承类的时候,实际上就会继承这个类的一些私有成员,既然继承了这个类的私有成员,你去定义这个类的时候就要实现这个私有成员。但是 ImageC 不是 Control 的子类,所以没法实现这个私有成员,所以它就缺少这个 state 属性,这样是不可以的。
所以说,当一个 接口 继承一个 类的类型 的时候,只有这个类或其子类才可以实现这个接口。像 Button 继承了 Control,它就可以实现 SelectableControl 接口。
ES6 Class 的相关概念与特性
类的核心是“面向对象”的思想,面向对象的三大特性:封装、继承 和 多态。
TS 类中可用的关键字:
public
:(默认)公有 父类(自身)以及子类和外部都能访问。protected
:受保护 父类(自身)以及子类都能访问 但是外部不能访问。private
:私有的 父类(自身)可以访问 但是子类和外部不能访问。super
关键字:用于对父类的直接引用,该关键字可以引用父类的属性和方法。static
关键字:用于定义类的数据成员(属性和方法)为静态的,静态成员可以直接通过类名调用。implements
关键字:类使用该关键字可以实现接口,并将 interest 字段作为类的属性使用。type
关键字:用于定义 类型别名 或 一个新的类型(包括:原始值、联合类型、元组以及其它任何你需要手写的类型——比如:字符串字面量类型)。abstract
关键字: 用于定义抽象类和在抽象类内部定义抽象方法抽象成员。readonly
关键字:只读属性。使用此关键字定义的属性,在类的外部以及类内部的实例方法里是不可以被修改的,在类的构造函数中才可以被修改。instanceof
运算符:用于判断对象是否是指定的类型,如果是返回 true,否则返回 false。类有 3 个成员:
constructor
方法是类的默认方法,类实例化时自动调用,可以为类的对象分配内存。constructor
方法,如果没有显式定义,一个空的 constructor
方法会被默认添加。定义一个类时,会创建两个东西:
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello' + this.greeting
}
}
let greeter = new Greeter('wolrd')
greeter.greet()
基类、父类、超类:被继承的类。
派生类、子类:继承而来的类。
类使用 extends
关键字继承,类中 this
表示当前对象本身,super
表父类对象。
类继承的实质:
this
,然后再将父类的方法添加到 this
上面 Parent.apply(this)
。this
上面,所以必须先调用 super
方法,然后再用子类的构造函数修改 this
。因为子类自己的 this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super
方法,子类就得不到 this
对象。类的继承的特点:
super()
方法来调用父类中的 构造函数,然后再进行子类的进一步初始化。//定义一个基类(超类)
class Animal {
move(distance: number = 0) {
console.log('Animal moved: ', distance);
}
}
//定义一个子类(派生类),继承自超类(基类)
class Dog extends Animal {
bark() {
console.log("Woof! Woof!");
}
}
const dog = new Dog()
dog.bark()
dog.move(10)
class Animal2 {
name: string
constructor(name: string) {
this.name = name
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m`);
}
}
//蛇
class Snake extends Animal2 {
constructor(name: string) {
super(name)//调用父类的构造方法
}
move(distance: number = 5) {
console.log('Slithering...');
super.move(distance)//调用父类的 move 方法
}
}
//马
class Horse extends Animal2 {
constructor(name: string) {
super(name)//调用父类的构造方法
}
move(distance: number = 45) {
console.log('Galloping...');
super.move(distance)//调用父类的 move 方法
}
}
let sam = new Snake('Sammy')
let tom: Animal = new Horse('Tommy')
//可以把 tom 指定为 Animal 类型,它虽然是 Animal 类型,但它实例化出来的值仍然是一个 Horse 子类的实例。
sam.move()
tom.move(20)
class Root {
str:string;
}
class Child extends Root {}
class Leaf extends Child {} // 多重继承,继承了 Child 和 Root 类
const obj = new Leaf();
obj.str ="hello"
console.log(obj.str)
【总结】类的继承的核心要点:
父类定义一个方法不去实现,让继承它的子类去实现,每一个子类可以有不同的表现——子类可以对父类的方法重新定义,这个过程称之为方法的重写,也是对类的多态的体现。
多态的特点:
class PrinterClass {
doPrint():void {
console.log("父类的 doPrint() 方法。")
}
}
class StringPrinter extends PrinterClass {
doPrint():void {
super.doPrint() // 调用父类的函数
console.log("子类的 doPrint()方法。")
}
}
public 修饰符:可以在类的内部外部任意使用,没有限制。
类中的属性和方法默认都是 public 的,可省略不写 public 修饰符。
class Animal3 {
public name: string
public constructor(name: string) {
this.name = name
}
public move(distance: number) {
console.log(`${this.name} moved ${distance}m`);
}
}
private 修饰符:可以在类的内部使用,但不能在类的外部使用。
class Animal4 {
private name: string
public constructor(name: string) {
this.name = name
}
public move(distance: number) {
console.log(`${this.name} moved ${distance}m`);
}
}
// new Animal4('Cat').name//报错了:属性“name”为私有属性,只能在类“Animal4”中访问。
TS 是结构类型的语言,当比较两种不同类型的类型的时候,并不在乎他们是从哪里来的,只要他们所有的成员的类型兼容的话,就认为他们是类型兼容的。但是,我们比较 private 和 protected 的成员的类型的时候,情况就不同了:
“如果其中一个类型包含一个 private 成员,那么只有另外一个类型也包含一个 private 成员,并且他们都是来自同一处声明的时候,我们才认为这两个是类型兼容的。”
而且,对于 protected 成员也适用这个规则。
以 private 为例:
class Animal5 {
private name: string
constructor(name: string) {
this.name = name
}
move(distance: number) {
console.log(`${this.name} moved ${distance}m`);
}
}
//河马
class Hippo extends Animal5 {
constructor() {
super('Hippo')
}
}
//大象
class Elephant {
private name: string
constructor(name: string) {
this.name = name
}
}
let animal = new Animal5('Goat')
let hippo = new Hippo()
let employee = new Elephant('Bob')
animal = hippo
// animal = employee//报错了:类型 "Employee" 中缺少属性 "move",但类型 "Animal" 中需要该属性。
为什么把 hippo 赋值给 animal 正常,把 employee 赋值给 animal 就报错了呢?
这是因为:
首先,要明确:hippo 是 Hippo 的实例, animal 是 Animal5 的实例,employee 是 Employee 的实例。
其次,Hippo 是 Animal5 的子类,Employee 不是 Animal5 的子类。Employee 里的 name 是其私有的成员,Animal5 里的 name 是其私有的成员,他们并不是同一个来源的 name。
但是,Hippo 和 Animal5 实际上是共享了 Animal5 的 name,他们的 name 来源是一致的。
所以,Hippo 和 Animal5 之间是兼容的,而 Employee 和 Animal5 是不兼容的。
毕竟,不同类的私有成员的来源是不一样的。
protected 修饰符:可以在类的内部使用,也可以在其派生类里使用,其他均不能使用。
//人类
class Person {
protected name: string
constructor(name: string) {
this.name = name
}
move(distance: number) {
console.log(`${this.name} moved ${distance}m`);
}
}
//成员
class Employee extends Person {
private department: string
constructor(name: string, department: string) {
super(name)
this.department = department
}
getElevatorPitch() {
return `hello my name is ${this.name} and I work in ${this.department}`
}
}
let howard = new Employee('Howard', 'Sales')
console.log(howard.getElevatorPitch());
// console.log(howard.name));//报错了:属性“name”受保护,只能在类“Person”及其子类中访问。
protected 还有一个用处:它可以给构造函数也标记为 protected。也就是说,我们为了防止外面的使用者去 new 这个 Person,可以把 Person 的构造函数也标记为 protected。
例如:
//人类
class Person2 {
protected name: string
protected constructor(name: string) {
this.name = name
}
move(distance: number) {
console.log(`${this.name} moved ${distance}m`);
}
}
//成员
class Employee2 extends Person {
private department: string
constructor(name: string, department: string) {
super(name)
this.department = department
}
getElevatorPitch() {
return `hello my name is ${this.name} and I work in ${this.department}`
}
}
let howard2 = new Employee2('Howard', 'Sales')
// let john = new Person2('John')//报错了:类“Person2”的构造函数是受保护的,仅可在类声明中访问。
class Person3 {
readonly name: string
constructor(name: string) {
this.name = name
}
}
let john2 = new Person3('John')
// john2.name = ''//报错了:属性“name”受保护,只能在类“Person”及其子类中访问。
参数属性:给 构造函数的参数 的“前面”添加 访问限定修饰符。
常见的“参数属性”包括: readonly、private、protected、public。
参数属性实际上是一种简写:在一个地方定义并初始化一个成员。
class Person4 {
constructor(readonly name: string) {}
}
let john3 = new Person4('John')
console.log(john3.name);
// john2.name = ''//报错了:无法为“name”赋值,因为它是只读属性。
虽然参数属性可以简化我们的书写,但是实际上我还是比较愿意在类的构造函数外面声明好属性,然后在构造函数里赋值,因为这样代码看起来更加清晰。
所以说,参数属性,只是在类中定义属性时写法的一个简化。
TS 支持 getters 和 setters 来对对象成员的访问的。
下面我们看看:如何把一个简单的类改写成 getter 和 setter 的形式?
先看一个没有使用存取器的例子:
class Employee3 {
fullName: string
}
let employee3 = new Employee3()
employee3.fullName = 'Bob Smith'
if(employee3.fullName) {
console.log(employee3.fullName);
}
有时候,我们想在设置 fullName 的时候触发一些额外的逻辑,这时候 存取器 就派上用场了。
额外的逻辑是:当我们去设置用户名的时候,我们想检测一下他的密码是否正确。密码匹配才可以设置这个用户名。
const passcode = 'secret passcode'
class Employee4 {
private _fullName: string
//访问
get fullName(): string {
return this._fullName
}
//修改
set fullName(newName: string) {
if(passcode && passcode === 'secret passcode') {
this._fullName = newName
} else {
console.log('Error: 未经授权更新员工!');
}
}
/**
* fullName 会报错:Accessors are only available when targeting ECMAScript 5 and higher.(访问器仅在以ECMAScript 5及更高版本为目标时可用。)
* 报错原因:tsc 默认编译的是 es3 的代码,而这里要求必须是 es5 以上。
* 解决方案:
* 在编译代码时,需要给 tsc 指定 es5 的编译版本:tsc exemples\03ts常用语法\03-6类\03-6-1类.ts --target es5
*/
}
let employee4 = new Employee4()
employee4.fullName = 'Bob Smith'
if(employee4.fullName) {
console.log(employee4.fullName);
}
类是由 实例部分 和 静态部分 两个部分组成的。到目前为止,我们只讨论了类的实例成员,但是我们也可以创建一些类的静态成员。
静态成员:属性存在于类的本身,而不是类的实例上。
TS 类中完全支持了 静态方法(ES6)和 静态属性(ES7)(注意:static 不能修饰构造函数)。
TS 通过在属性和方法前面加上 static
关键字来定义静态的属性和方法。
静态成员的特点:
案例需求:定义一个网格类,计算一个坐标点距离原点的距离。
class Grid {
//原点是可以被网格共享的,所以给它设置为静态属性
static origin = { x: 0, y: 0 }
//成员属性
scale: number
constructor(scale: number) {
this.scale = scale
}
calculateDistanceFromOrigin(point: {x:number, y:number}) {
//静态属性需要用类去访问
let xDist = point.x - Grid.origin.x
let yDist = point.y - Grid.origin.y
//勾股定理
return Math.sqrt(xDist * xDist + yDist * yDist) * this.scale
}
}
let grid1 = new Grid(1.0)
let grid2 = new Grid(5.0)
console.log(grid1.calculateDistanceFromOrigin({x:3, y:4}));
console.log(grid2.calculateDistanceFromOrigin({x:3, y:4}));
用 abstract
关键字标识的类是抽象类。
抽象类的用途(特点):
抽象类除了包含普通的成员,还包含抽象方法,用 abstract
关键字标识的方法是抽象方法。
抽象方法的特点:
abstract class Department2 {
name: string
constructor(name: string) {
this.name = name
}
printName(): void {
console.log('Department name' + this.name);
}
abstract printMeeting(): void
}
class AccountingDepartment extends Department2 {
constructor() {
super('Accounting add Auditing')
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am')
}
genterateReports(): void {
console.log('Generating accounting reports...');
}
}
let department2: Department2
// department2 = new Department2()
//报错了:无法创建抽象类的实例。
//抽象类不能被实例化,但是其派生类是可以的。
department2 = new AccountingDepartment()
department2.printName()
department2.printMeeting()
// department2.genterateReports()
//报错了:类型“Department2”上不存在属性“genterateReports”。
这里就有一个疑问:话说 department2 是 AccountingDepartment 的一个实例,怎么还不能用 AccountingDepartment 里面的 genterateReports 方法了呢?
这是因为:我们事先已经将 department2 声明为抽象类 Department2 这样的一个类型了。抽象类 Department2 中是没有定义这个 genterateReports 方法的,所以才会报这个错误的。
适用 typeof
关键字可以快速创建一个类的副本。
class Greeter3 {
static standardGreeting= 'Hello, there'
greeting: string
constructor(message?: string) {
this.greeting = message || ''
}
greet() {
if(this.greeting) {
return 'Hello, ' + this.greeting
} else {
return Greeter3.standardGreeting
}
}
}
let greeter3: Greeter3
greeter3 = new Greeter3()
console.log(greeter3.greet());
//给 Greeter3 类起别名(快速创建一个类的副本)
let greeterMaker: typeof Greeter3 = Greeter3
greeterMaker.standardGreeting = 'Hey, there'
let greeter3_2: Greeter3 = new greeterMaker()
console.log(greeter3_2.greet());
interface Point {
x: number
y: number
}
interface Point3d extends Point {
z: number
}
当然,我们还可以把 Point 的 interface 换成 class,将类作为接口使用:
class Point2 {
x: number
y: number
}
interface Point3d2 extends Point2 {
z: number
}
let point3d2: Point3d2 = {x: 1, y: 2, z: 3}
虽然我们可以将类作为接口使用,但是通常不建议这样去用。因为:如果它没有 “类具有的特性” 需要定义的话,只是单纯的包含一些签名属性,还是建议直接把它定义为接口。
implements
)详见本文的 “接口-类类型” 部分。
TypeScript 函数保留了所有 JavaScript 函数的功能,所以,在 TS 中,完全可以像 JS 那样去使用函数。不过,TS 为 JS 函数添加了额外的类型校验功能,这使得 TS 函数的功能更加强大。
//命名函数
function add(x, y) {
return x + y
}
//匿名函数
let myAdd = function(x, y) {
return x + y
}
//匿名函数也是可以定义函数名的
let myAdd2 = function add2 (x, y) {
return x + y
}
函数类型 包括 参数类型 和 返回值类型 两部分。
参数:数据类型
:可以指定函数的参数类型。参数?:数据类型
:用来定义可选参数。参数=一个常量
:用来定义参数的默认值。以防没有传入型参 a 对应的实参而产生未定义。...参数:参数类型[]
:用来表示剩余参数。void
而不能留空)。any
或 void
,那么可以没有返回值,否则该函数必须要返回值,即必须包含 return
语句。参数类型有两种实现形式:
let myAdd5 = function(x: number, y: number): number {
return x + y
}
myAdd5(1, 2)
let myAdd6: (baseValue: number, increment: number) => number = function(x, y) {
return x + y
}
myAdd6(1, 2)
参数类型的重命名:只要参数个数、位置、类型是匹配的就可以,参数名(入参与形参)不一样没事。
例如:
let myAdd4: (baseValue: number, increment: number) => number = function(x: number, y: number): number {
return x + y
}
在 js 中函数的每个参数都是可选的,不传的时候默认是 undefined。但在 ts 中函数的每个参数默认都是必须传的,不传就会报错。也就是传入的参数的个数必须和函数所期望的个数是一致的。
function builName(firstName: string, lastName: string): string {
return firstName + ' ' + lastName
}
// let result1 = builName('Bob')// 少了——报错了:应有 2 个参数,但获得 1 个。
// let result2 = builName('Bob', 'Adams', 'Sr')// 多了——报错了:应有 2 个参数,但获得 3 个。
let result3 = builName('Bob', 'Adams')
在 ts 中定义函数时,可以使用 ?
符号实现可选参数的功能。可选参数必须放在其他参数的后面。
function builName2(firstName: string, lastName?: string): string {
if(lastName) {
return firstName + ' ' + lastName
} else {
return firstName
}
}
为参数提供默认值,当用户不传的时候,它会得到这个默认值。
function builName3(firstName: string, lastName = 'Smith'): string {
return firstName + ' ' + lastName
}
用剩余运算符(…)定义函数的剩余参数,可以把其余的参数均收进一个变量里。可以一个都没有,也可以是任意多个。
function builName4(firstName: string, ...restOfName: string[]): string {
let [lastName] = restOfName
if(lastName) {
return firstName + ' ' + lastName
} else {
return firstName
}
}
let result1_3 = builName4('Bob')
let result2_3 = builName4('Bob', 'Adams', 'Sr')
let result3_3 = builName4('Bob', 'Adams')
当然,剩余运算符(…)还可以在带有剩余参数的函数定义时使用到。
let builNameFn: (fname: string, ...rest: string[]) => string = builName4
TS 默认会自动推断出函数类型的。
let myAdd6: (baseValue: number, increment: number) => number = function(x, y) {
return x + y
}
myAdd6(1, 2)
// myAdd6('', 2)//报错了:类型“string”的参数不能赋给类型“number”的参数。
let deck = {
suits: ['红心', '黑桃', '草花', '方片'],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return {
suit: this.suits[pickedSuit],
card: pickedCard % 13
}
}
}
}
let cardPicker = deck.createCardPicker()
// let pickedCard = cardPicker()
// console.log('Card: ' + pickedCard.card + ' of ' + pickedCard.suit);
//执行后报错:TypeError: Cannot read property '1' of undefined
这是因为:首先,cardPicker 是 deck.createCardPicker 函数,执行 cardPicker 函数时,this 指向的是全局的 global。 global 里面显然是没有 suits 这个属性的。所以 this.global 就是 undefined,再去访问它的下标就会报错了。为了解决这个问题,就要保证这个 this 的指向是正确的。在 es6 里我们可以使用箭头函数来解决这个问题。
let deck2 = {
suits: ['红心', '黑桃', '草花', '方片'],
cards: Array(52),
createCardPicker: function() {
//箭头函数:不会改变 this 指向
return () => {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return {
suit: this.suits[pickedSuit],
card: pickedCard % 13
}
}
}
}
let cardPicker2 = deck2.createCardPicker()
let pickedCard2 = cardPicker2()
// console.log('Card: ' + pickedCard2.card + ' of ' + pickedCard2.suit);
// 此时,cardPicker2 函数执行时,this 指向的是 deck2 这个对象了。
虽然 箭头函数 能够解决上述代码中 this 指向的问题,但是在 ts 中该对象字面量的这个 this 是被默认推断为 any 类型的,也就是说你访问 this.suits.[任意属性]
时都是可以通过的。这就会导致没有类型校验了,我们可以给函数提供一个显式的 this 参数。
this 参数是一个假的参数,它是出现在参数列表最前面。它告诉你这个 this 是不可用的。
function f(this: void) {}
这样的话,他就能确保,这个 this 在这个独立函数汇总是一个空的、不可用的一个状态。
interface Card {
suit: string
card: number
}
interface Deck {
suits: string[]
cards: number[]
createCardPicker(this: Deck): () => Card
}
let deck3: Deck = {
suits: ['红心', '黑桃', '草花', '方片'],
cards: Array(52),
createCardPicker: function(this: Deck) {
//箭头函数:不会改变 this 指向
return () => {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return {
suit: this.suits[pickedSuit],
card: pickedCard % 13
}
}
}
}
let cardPicker3 = deck3.createCardPicker()
let pickedCard3 = cardPicker3()
console.log('Card: ' + pickedCard3.card + ' of ' + pickedCard3.suit);
有时我们在回调函数里面调用 this 会报错,特别是我们在使用一些第三方库的时候,那其实我们也以在第三方库中为这个 this 的回调函数指定 this 参数。
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void
}
class Handler {
type: string
onClickBad(this: Handler, e: Event) {
console.log('clicked');
this.type = e.type
}
}
let h = new Handler()
let uiElement: UIElement = {
addClickListener() {
}
}
// uiElement.addClickListener(h.onClickBad)
//报错了:
// 类型“(this: Handler, e: Event) => void”的参数不能赋给类型“(this: void, e: Event) => void”的参数。
// 每个签名的 "this" 类型不兼容。
// 不能将类型“void”分配给类型“Handler”。
这是因为:首先,uiElement 是 UIElement 的实例,那么,UIElement 定义的 addClickListener 回调函数的函数类型的 this 是 void,但是我们在 uiElement.addClickListener 给他传进去的函数对应的 this 是 Handler,显然我们不能把 Handler 类型赋值给 void 类型,类型就不匹配了。怎么办呢?我们可以显示的把类型改为 void,这样类型就一致了。
interface UIElement2 {
addClickListener(onclick: (this: void, e: Event) => void): void
}
class Handler2 {
type: string
//显示的把类型改为 void,这样类型就匹配了
onClickBad(this: void, e: Event) {
console.log('clicked');
// this.type = e.type
//这里报错了:类型“void”上不存在属性“type”。
}
}
let h2 = new Handler2()
let uiElement2: UIElement2 = {
addClickListener() {
}
}
uiElement2.addClickListener(h2.onClickBad)
上述代码报错是因为:onClickBad 的 this 的类型是 void,所以就不能访问这个 this.type,是访问不到的。但是有些情况我们又想去访问 this,又要保证类型是匹配的,那怎么办呢?我们可以用箭头函数来解决:
interface UIElement3 {
addClickListener(onclick: (this: void, e: Event) => void): void
}
class Handler3 {
type: string
//显示的把类型改为 void,这样类型就匹配了
onClickBad = (e: Event) => {
console.log('clicked');
this.type = e.type
}
}
let h3 = new Handler3()
let uiElement3: UIElement3 = {
addClickListener() {
}
}
uiElement3.addClickListener(h3.onClickBad)
这样是用箭头函数,就能满足:我们又想去访问 this,又要保证类型是匹配的。
【总结】:
TS 函数重载指的是:函数的名字相同,函数形参的 类型、 个数 和 顺序 不同。
函数重载用于约定:函数参数的类型和个数,以及函数返回值的类型。
函数重载,需要声明函数函数的签名,即:只定义而不实现这个函数。
function add (s: string): void;
function add (n: number): void;
function add (n1: number): void;
function add (x: number, y: number): void;
function add (n: number, s: string): void;
function add (s: string, n: number): void;
函数重载的特点:
函数重载的技巧:
下面举个例子,先从一个 js 函数的场景切入:
js 是一个动态语言,js 函数会根据不同的参数返回不同的类型的数据。
let suits = ['红心', '黑桃', '草花', '方片']
function pickCard(x): any {
if(Array.isArray(x)) {
let pickedCard = Math.floor(Math.random() * x.length)
return pickedCard
} else if(typeof x == 'number') {
let pickedSuit = Math.floor(x / 13)
return { suit: suits[pickedSuit], card: x % 13 }
}
}
let myDeck = [
{ suit: '方片', card: 2 },
{ suit: '黑桃', card: 10 },
{ suit: '红心', card: 4 },
]
let pickedCard1_1 = myDeck[pickCard(myDeck)]
console.log('card: ' + pickedCard1_1.card + ' of ' + pickedCard1_1.suit);
let pickedCard1_2 = pickCard(15)
console.log('card: ' + pickedCard1_2.card + ' of ' + pickedCard1_2.suit);
//这样写是有缺陷的:没有对 pickCard 做有效的类型检查。我们甚至传入一个字符串都没有报错的,因为 pickCard 没有做参数类型的约束。
// let pickedCard1_3 = myDeck[pickCard('sss')]
对于这种场景,我们要通过函数重载的方式来做约束。
let suits2 = ['红心', '黑桃', '草花', '方片']
//重载函数的声明
function pickCard2(x:{suit: string, card: number}[]): number
function pickCard2(x: number): {suit: string, card: number}
//重载函数时,可以重新自定义函数的参数和返回值。
//重载函数的实现
function pickCard2(x): any {
if(Array.isArray(x)) {
let pickedCard = Math.floor(Math.random() * x.length)
return pickedCard
} else if(typeof x == 'number') {
let pickedSuit = Math.floor(x / 13)
return { suit: suits[pickedSuit], card: x % 13 }
}
}
let myDeck2 = [
{ suit: '方片', card: 2 },
{ suit: '黑桃', card: 10 },
{ suit: '红心', card: 4 },
]
let pickedCard2_1 = myDeck2[pickCard2(myDeck2)]
console.log('card: ' + pickedCard2_1.card + ' of ' + pickedCard2_1.suit);
let pickedCard2_2 = pickCard(15)
console.log('card: ' + pickedCard2_2.card + ' of ' + pickedCard2_2.suit);
// let pickedCard2_3 = myDeck2[pickCard2('sss')]
//报错了:没有与此调用匹配的重载。
// 第 1 个重载(共 2 个),“(x: { suit: string; card: number; }[]): number”,出现以下错误。
// 类型“string”的参数不能赋给类型“{ suit: string; card: number; }[]”的参数。
// 第 2 个重载(共 2 个),“(x: number): { suit: string; card: number; }”,出现以下错误。
可见,此时就可以做类型检查了:再传入一个字符串,就不满足重载后的函数的任何一种定义了。
【注意】:编译器对重载的处理是“依次重试匹配”的,所以我们在做重载定义的时候,要把最精确的放在前面。这是为了让编译器能够正确高效的做类型检查。
假设我们有一个需求:定义一个add函数,它可以接收两个都是string类型的参数,返回拼接后的结果;也可以接收两个都是number类型的参数,返回两数相加之和。
先看不用函数重载,而是通过联合类型,来实现:
// 通过联合类型声明函数
function add(a1: number | string, a2: number | string): string | number {
if (typeof a1 === "number" && typeof a2 === "number") {
return a1 + a2
} else if (typeof a1 === "string" && typeof a2 === "string") {
return a1 + a2
}
}
// 调用这个函数
console.log(add('qwer', 'asdf')) // qwerasdf
console.log(add(123, 456)) // 579
console.log(add('qwer', 123)) // undefined
由上述代码可知,通过联合类型来实现这个需求,有两个缺点:
而函数重载恰好能解决这两个问题:
// 通过函数重载声明函数的签名
function add(num1: number, num2: number): number;
function add(num1: string, num2: string): string;
// 实现这个函数
function add(num1: any, num2: any): any {
return num1 + num2
}
// 调用这个函数
console.log(add('qwer', 'asdf')) // qwerasdf
console.log(add(123, 456)) // 579
// console.log(add('qwer', 123)) // 报错:没有与此调用匹配的重载
详见上文的 “接口–>定义一个接口–>额外的属性检查” 小节。
泛型指:在定义函数、接口或类时,无法预先确定要使用的数据的类型,而是在使用函数、接口或类时才能确定数据的类型。
泛型主要用来满足:函数能够支持多种类型的数据。这样用户就可以用自己的数据类型来使用函数。
泛型的核心思想是:类型变量。
我们使用 any 来定义一个函数:
function fn(arg: any): any {
return arg;
}
由于使用 any 类型会导致这个函数可以接收任何类型的参数,那么也就可能返回任何数据类型的值。这样就导致:可能会出现传入的参数类型与返回值的类型不匹配的情况。
比如:
function fn(arg: any): any {
return arg + ''
}
fn(123)//"123"
此时,传入的是 number 类型的参数,返回的却是 string 类型的结果。
那么,此时如何保持参数类型与返回值类型是相同的呢?可以使用“类型变量”,来保持参数类型与返回值类型是相同的。
用 类型变量 重写 fn 函数:我们给 fn 添加了类型变量 T 来捕获用户传入的类型。
function fn<T>(arg: T): T {
return arg;
}
这就是 泛型 的基本实现了。传入什么类型就返回什么类型。适用于多个类型。不同于 any,any 会丢失类型信息,而泛型不会丢失类型信息。
function identity4<T>(arg: T): T {
return arg
}
//使用方式一
let output = identity4<string>('myString')
//使用方式二:
let output2 = identity4('myString')//利用编译器帮我们自动进行类型推断
对于一些复杂的情况,编译器有时候是不能帮我们自动的类型推断的,此时就只能通过第一种方式来使用泛型了。
function identity5<T>(arg: T): T {
return arg
}
function loggingIdentity<T>(arg: T): T {
// console.log(arg.length);
//报错了:类型“T”上不存在属性“length”。
return arg
}
上述代码报错了,这是因为 arg 是任意类型的,如果我们传入的是一个数值,显然 number 类型是没有 length 属性的。
如果你想操作这个 T,而且这个 T 是有 length 的,那么可以把这个类型 T 作为一个类型 T 的数组。
function loggingIdentity2<T>(arg: T[]): T[] {
console.log(arg.length);
return arg
}
也就是说,这个泛型函数,可以接收类型参数 T 和参数 arg,参数 arg 的类型是 T[]
,并且这个泛型函数的返回值的类型也是 T[]
。
如果我们传入的是一个 number 类型的数组,它就会返回一个 number 类型的数组,所以 T 的类型就是一个 number,这样就可以:
把 泛型变量 T 当做类型的一部分使用,而不是作为整个类型使用,增加了灵活性。
我们已经知道,泛型函数只是在普通函数的基础上,在参数的前面增加了 类型参数 ,其他与普通函数没什么区别。
function identity6<T>(arg: T): T {
return arg;
}
那么如何定义 泛型类型(泛型函数的类型参数) 呢?
泛型类型 的特点:
我们也可以:用一个 “带有 调用签名 的对象字面量” 来定义 “泛型类型”。
function identity6<T>(arg: T): T {
return arg
}
//用带有 调用签名 的 箭头函数 定义 泛型类型
let myIdentity: <U>(arg: U) => U = identity6
//用带有 调用签名 的 对象字面量 定义 泛型类型
let myIdentity2: {<T>(arg: T): T} = identity6
我们通常基于 “用一个带有调用签名的对象字面量来定义泛型类型” 的实现方式,来定义泛型接口。
function identity6<T>(arg: T): T {
return arg
}
interface FenericIdentityFn {
<T>(arg: T): T
}
let myIdentity3: FenericIdentityFn = identity6
在定义泛型接口时,可以把 T 拿出来作为接口的参数。把参数拿出来之后,在使用该泛型接口时,就需要指明使用的是什么类型。传入什么类型就返回什么类型。
function identity6<T>(arg: T): T {
return arg
}
interface FenericIdentityFn2<T> {
(arg: T): T
}
let myIdentity4: FenericIdentityFn2<number> = identity6
这样做的好处是:我们就不用在这个泛型接口中描述这个泛型函数了。而是把这个非泛型函数签名,作为泛型类型的一部分。然后,我们使用 FenericIdentityFn2 泛型接口的时候,需要传入一个类型参数来指定这个泛型的类型。推荐这样使用泛型接口。
泛型类要放在 类名的后面。用于:确认类中的所有属性都可以使用这个类型。
class GgenericNumber<T> {
zeroValue: T
add: (x: T, y: T) => T
}
let myGenericNumber = new GgenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function (x, y) {
return x + y
}
let stringNumberic = new GgenericNumber<string>()
stringNumberic.zeroValue = ''
stringNumberic.add = function (x, y) {
return x + y
}
类包含两个部分:静态部分和实例部分。泛型类实际上指的是实例部分的一个类型,静态属性是不能使用泛型类的。
function loggingIdentity3<T>(arg: T): T {
// console.log(arg.length);
//报错了:类型“T”上不存在属性“length”。
return arg
}
回顾上面这个案例,当时给出的解决方案是,将 T 类型变成一个 T 类型的数组,因为数组是有 length 属性的。实际上还有另外一种解决方案:可以给类型变量 T 做 “类型约束”。
通过接口约束泛型函数的类型变量:
interface Lengthwise {
length: number
}
function loggingIdentity4<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg
}
这样就不报错了。
此时,在使用该泛型函数时,若传入的参数的类型不符合签名的约束就会报错。
// loggingIdentity4(3)
//报错了:类型“number”的参数不能赋给类型“Lengthwise”的参数。
loggingIdentity4({length: 1})
这是因为,当我们调这个方法的时候,它会推断这个类型。把这个类型赋值给 T,T 实际上是被 Lengthwise 接口约束的。
比如:现在有一个需求是查询对象的某一个值。对象用任意类型 T 约束,我们希望这个属性 key 是存在于这个对象中,所以也要对 key 进行约束。所以我们再定义一个类型参数 K 来约束属性 key,K extends keyof T
,K 是被 T 的 key 所约束的,即 K 类型必须是 T 类型里的属性。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
let x = {a: 1, b: 2, c: 3, d: 4}
getProperty(x, 'a')
// getProperty(x, 'm')
//报错了:类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数。
比如:创建一个 Create 函数,传入一个类的实例类型 T。这个就是工厂函数的一个构造器,构造器的参数是一个构造器类型,所以它是一个 {new(): T}
——构造器返回的是一个类的实例类型 T。这个工厂函数返回值类型也是一个类的实例类型 T。它的实现就是return new c()
。这就是类类型在工厂函数中的一个应用。
function Create<T>(c: {new(): T}): T {
return new c()
}
来看一个更高级的例子。比如:我们定义一些动物和动物管理员。
class BeeKeeper {
hasMask: boolean
}
class LionKeeper {
nametag: string
}
class Animal {
numLengs: number
}
class Bee extends Animal {
keeper: BeeKeeper
}
class Lion extends Animal {
keeper: LionKeeper
}
function createInstance<T extends Animal>(c: new() => T) :T {
return new c()
}
// createInstance(Lion).keeper.nametag
// createInstance(Bee).keeper.hasMask
利用泛型约束推导的一个属性,很容易推断出你的成员的一个类型是什么样子的——这就是泛型约束的好处。
TypeScript 支持内嵌,类型检查以及将 JSX 直接编译为 JavaScript。
使用 JSX 必须做两件事:
TypeScript具有三种JSX模式:preserve、react 和 react-native。
模式 | 输入 | 输出 | 输出文件扩展名 |
---|---|---|---|
preserve | < div /> | < div /> | .jsx |
react | < div /> | React.createElement(“div”) | .js |
react-native | < div /> | < div /> | .js |
你可以通过在命令行里使用 --jsx
标记 或 tsconfig.json 里的选项 来指定模式。
【注意】React标识符是写死的硬编码,所以你必须保证React(大写的R)是可用的。
as 操作符在 .ts 和 .tsx 里都可用,并且与尖括号类型断言行为是等价的。但是在 .tsx 里只可以使用 as 操作符来进行类型断言。
var foo = bar as foo;
其他请看官网的 JSX。
除了传统的面向对象继承方式,还可以通过 Mixins 来重用组件。
例如:
// 一次性的 Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// 可激活的 Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
}
interact() {
this.activate();
}
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
// 在运行库的某个地方
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
代码里首先定义了 Disposable 和 Activatable 两个类,它们将做为 mixins。 然后创建一个 SmartObject 类,结合了这两个 mixins。首先应该注意到的是,没使用 extends 而是使用 implements。 把类当成了接口,仅使用 Disposable 和 Activatable 的类型而非其实现。 这意味着我们需要在类里面实现接口。 但是这是我们在用 mixin 时想避免的:
// 一次性的
isDisposed: boolean = false;
dispose: () => void;
// 可激活的
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
最后,把mixins混入定义的类,完成全部实现部分。
applyMixins(SmartObject, [Disposable, Activatable]);
最后,创建这个帮助函数,帮我们做混入操作。 它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
})
});
}
【推荐】
如何进阶TypeScript功底?一文带你理解TS中各种高级语法
【参考文章】
TypeScript 官方文档
TypeScript 教程
TypeScript 入门教程
深入理解 TypeScript
TypeScript语法总结+项目(Vue.js+TS)实战