typescript笔记
未完待续…
基础类型
字符串
我们可以使用string
表示 JavaScript 中任意的字符串
let firstname: string = 'Captain'; // 字符串字面量
let familyname: string = String('S'); // 显式类型转换
let fullname: string = `my name is ${firstname}.${familyname}`; // 模板字符串
数字
支持的十进制整数、浮点数,以及二进制数、八进制数、十六进制数
/** 十进制整数 */
let integer: number = 6;
/** 十进制整数 */
let integer2: number = Number(42);
/** 十进制浮点数 */
let decimal: number = 3.14;
/** 二进制整数 */
let binary: number = 0b1010;
/** 八进制整数 */
let octal: number = 0o744;
/** 十六进制整数 */
let hex: number = 0xf00d;
较少的大整数,那么我们可以使用bigint
类型来表示
let big: bigint = 100n;
虽然number
和bigint
都表示数字,但是这两个类型不兼容。
布尔值
我们可以使用boolean
表示 True 或者 False
let TypeScriptIsGreat: boolean = true;
let TypeScriptIsBad: boolean = false
Symbol
ts支持Symbol原始类型,即我们可以通过Symbol创建一个独一无二的标记
let sym1: symbol = Symbol();
let sym2: symbol = Symbol('42');
Array
我们也可以像 JavaScript 一样定义数组类型,并且指定数组元素的类型
// 子元素是数字类型的数组
let arrayOfNumber: number[] = [1, 2, 3];
// 子元素是字符串类型的数组
let arrayOfString: string[] = ['x', 'y', 'z']
元组类型(Tuple)
let tom: [string, number] = ['Tom', 25];
const teacherList: [string, string, number][] = [
['dell', 'male', 20],
['hansen', 'male', 22]
]
any
any 指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式
注解为 any 类型的变量进行任何操作,包括获取事实上并不存在的属性、方法,并且 TypeScript 还无法检测其属性是否存在、类型是否正确
不过切记,避免使用any, Any is Hell(Any 是地狱),因此,除非有充足的理由,否则我们应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。
unknown
与 any 不同的是,unknown 在类型上更安全。比如我们可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any
let result: unknown;
let num: number = result; // 提示 ts(2322)
let anything: any = result; // 不会提示错误
void\undefined\null
这个三个应该用不到
注意:我们可以把 undefined 值或类型是 undefined 的变量赋值给 void 类型变量,反过来,类型是 void 但值是 undefined 的变量不能赋值给 undefined 类型
never
never 表示永远不会发生值的类型
function ThrowError(msg: string): never {
throw Error(msg);
}
never 是所有类型的子类型,它可以给所有类型赋值。
let Unreachable: never = 1; // ts(2322)
Unreachable = 'string'; // ts(2322)
Unreachable = true; // ts(2322)
let num: number = Unreachable; // ok
let str: string = Unreachable; // ok
let bool: boolean = Unreachable; // ok
我们可以把 never 作为接口类型下的属性类型,用来禁止写接口下特定的属性
const props: {
id: number,
name?: never
} = {
id: 1
}
props.name = null // (2322)
props.name = 'str' // (2322)
props.name = 1; // (2322)
无论我们给 props.name 赋什么类型的值,它都会提示类型错误,实际效果等同于 name 只读
object
它也是个没有什么用武之地的类型
declare function create(o: object | null): any;
create({}); // ok
create(() => null); // ok
create(2); // ts(2345)
create('string'); // ts(2345)
类型断言
有时候我们遇到TS类型不发检测的情况,如下所示
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 Type 'undefined' is not assignable to type 'number'
解释:greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number
不过,我们可以使用一种笃定的方式——类型断言
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
又使用尖括号 + 类型的格式做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = <number>arrayNumber.find(num => num > 2);
上两种方式虽然没有任何区别,但是尖括号格式会与 JSX 产生语法冲突,因此我们更推荐使用 as 语法。
此外还有一种特殊非空断言,即在值(变量、属性)的后边添加 ‘!’ 断言操作符,它可以用来排除值为 null、undefined 的情况。这也是类型断言的一种方式方法。
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
在复杂应用场景中,如果我们使用非空断言,就无法保证之前一定非空的值,比如页面中一定存在 id 为 feedback 的元素,数组中一定有满足 > 2 条件的数字,这些都不会被其他人改变。而一旦保证被改变,错误只会在运行环境中抛出,而静态类型检测是发现不了这些错误的。
函数类型:返回值类型和参数类型到底如何定义?
ts中函数是最基本、最重要的元素
定义一
function add() {}
const add1 = () => {}
定义二
const add = (a: number, b: number): number => {
return a + b
}
参数名后的 ‘:number’ 表示参数类型都是数字类型,圆括号后的 ‘: number’ 则表示返回值类型也是数字类型。
返回值类型
在 JavaScript 中,我们知道一个函数可以没有显式 return,此时函数的返回值应该是 undefined
function fn() {
// TODO
}
console.log(fn()); // => undefined
需要注意的是,在 TypeScript 中,如果我们显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒。
function fn(): undefined {}// A function whose declared type is neither 'void' nor 'any' must return a value.(2355)
正确的做法是使用void 类型来表示函数没有返回值的类型。
需要注意的是,这里的=>与 ES6 中箭头函数的=>有所不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。
type Adder = (a; number, b: number) => number;// TS 函数类型定义
const add: Adder = (a, b) => a + b// ES6箭头函数
我们还可以使用类似对象属性的简写语法来声明函数类型的属性,如下代码所示:
interface Entity {
add: (a: number, b: number) => number;
del(a: number, b: number): number
}
const entity: Entity = {
add: (a, b) => a + b,
del(a, b) {
return a - b
}
}
可缺省和可推断的返回值类型
函数返回值的类型可以在 TypeScript 中被推断出来,即可缺省。
interface Entity {
add: (a: number, b: number) => number;
del(a: number, b: number): number
}
const entity: Entity = {
add: (a, b) => a + b,
del(a, b) {
return a - b
}
}
function computeTypes(one: string, two: number) {
const nums = [two];
const strs = [one];
return {
nums,// (property) nums: number[]
strs// (property) strs: string[]
}
}
函数返回值的类型推断结合泛型,可以实现特别复杂的类型计算(本质是复杂的类型推断)
参数类型
可选参数、默认参数、剩余参数的学习
可选参数
我们的函数参数可传可不传,当然 TypeScript 也支持这种函数类型表达。
function log(x?: string) {
return x
}
log()// function log(x?: string): string | undefined
log('hello world')
说明上面的x返回可能是string 或 undefined,但是注意了,我们不能显式的传入参数
function log(x?: string) {
console.log(x);
}
function log1(x: string | undefined) {
console.log(x);
}
log();
log(undefined);
log1(); // ts(2554) Expected 1 arguments, but got 0
log1(undefined);
这里的 ?: 表示参数可以缺省、可以不传,也就是说调用函数时,我们可以不显式传入参数。但是,如果我们声明了参数类型为 xxx | undefined,就表示函数参数是不可缺省且类型必须是 xxx 或者 undfined。
默认参数
TypeScript 会根据函数的默认参数的类型来推断函数参数的类型
function log(x = 'hello') {
console.log(x)
}
log(); // => 'hello'
log('hi'); // => 'hi'
log(234)// Argument of type 'number' is not assignable to parameter of type 'string'.(2345)
因为上面x定义的类型式字符串类型,所以,下面传入234数字类型就会报错。
函数的默认参数类型必须是参数类型的子类型(如下代码)
function log3(x: number | string = 'hello') {
console.log(x);
}
函数 log3 的函数参数 x 的类型为可选的联合类型 number | string,但是因为默认参数字符串类型是联合类型 number | string 的子类型,所以 TypeScript 也会检查通过。
剩余参数
function sum2(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0)
}
sum2(1, 2)
sum2(1, 2, 3)
sum2(2, '3')// Argument of type 'string' is not assignable to parameter of type 'number'
以上代码’3’不是number 类型
我们将函数参数 nums 聚合的类型定义为 (number | string)[] 就不会出问题了
function sum2(...nums: (number|string)[]) {
return nums.reduce<number>((a, b) => a + (Number(b)), 0)
}
sum2(1, 2)
sum2(1, 2, 3)
sum2(2, '3')
this
在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,比如最简单的作为对象的方法的 this 指向,如下代码所示:
function say(this: Window, name: string) {
console.log(this.name)
}
window.say = say
window.say('hello')
const obj = {
say
}
// The 'this' context of type '{ say: (this: Window, name: string) => void; }' is not assignable to method's 'this' of type 'Window'.
obj.say('hello')
在上述代码中,我们在 window 对象上增加 say 的属性为函数 say。那么调用window.say()
时,this 指向即为 window 对象。
调用obj.say()
后,此时 TypeScript 检测到 this 的指向不是 window,于是抛出了如下所示的一个 ts(2684) 错误。
say('captain'); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Window'
注意: 如果我们直接调用 say(),this 实际上应该指向全局变量 window,但是因为 TypeScript 无法确定 say 函数被谁调用,所以将 this 的指向默认为 void,也就提示了一个 ts(2684) 错误。
此时,我们可以通过调用 window.say() 来避免这个错误,这也是一个安全的设计。因为在 JavaScript 的严格模式下,全局作用域函数中 this 的指向是 undefined。
同样,定义对象的函数属性时,只要实际调用中 this 的指向与指定的 this 指向不同,TypeScript 就能发现 this 指向的错误,示例代码如下:
interface Person {
name: string;
say(this: Person): void;
}
const person: Person = {
name: 'captain',
say() {
console.log(this.name);
}
}
const fn = person.say;
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'.(2684)
fn()
很明显上面的fn执行是指向person的,但是很明显它指向了window
注意了,显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉,这算是 TypeScript 为数不多的特有语法。
如下所示:
function say(name) {
console.log(this.name);
}
链式调用风格的库中,使用 this 也可以很方便地表达出其类型
class Container {
private val: number;
constructor(val: number) {
this.val = val;
}
// cb是一个回调函数
map(cb: (x: number) => number): this {
this.val = cb(this.val);
return this;
}
log(): this {
console.log(this.val);
return this;
}
}
const instance = new Container(1)
.map((x) => x + 1)
.log() // => 2
.map((x) => x * 3)
.log(); // => 6
函数重载
在 TypeScript 中,也可以相应地表达不同类型的参数和返回值的函数,也就是说,函数名称相同,参数数量或类型不同, 或者参数数量相同同时参数顺序不同
function convert(x: string | number | null): string | number | -1 {
if (typeof x === 'string') {
return Number(x);
}
if (typeof x === 'number') {
return String(x);
}
return -1;
}
const x1 = convert('1'); // => string | number
const x2 = convert(1); // => string | number
const x3 = convert(null); // => string | number
在上述代码中,我们把 convert 函数的 string 类型的值转换为 number 类型,number 类型转换为 string 类型,而将 null 类型转换为数字 -1。此时, x1、x2、x3 的返回值类型都会被推断成 string | number 。
那么,有没有一种办法可以更精确地描述参数与返回值类型约束关系的函数类型呢?有,这就是函数重载。
function convert(x: string): number;
function convert(x: number): string;
function convert(x: null): -1;
function convert(x: string | number | null): any {
if (typeof x === 'string') {
return Number(x);
}
if (typeof x === 'number') {
return String(x);
}
return -1;
}
const x1 = convert('1'); // => number
const x2 = convert(1); // => string
const x3 = convert(null); // -1
注意:函数重载列表的各个成员(即示例中的 1 ~ 3 行)必须是函数实现(即示例中的第 4 行)的子集,例如 “function convert(x: string): number”是“function convert(x: string | number | null): any”的子集。
下面写了一段内容方便自己理解
interface P1 {
name: string;
}
interface P2 extends P1 {
age: number;
}
function convert(x: P1): number;
function convert(x: P2): string;
function convert(x: P1 | P2): any {}
const x1 = convert({ name: "" } as P1); // => number
const x2 = convert({ name: "", age: 18 } as P2); // number
因为 P2 继承自 P1,所以类型为 P2 的参数会和类型为 P1 的参数一样匹配到第一个函数重载,此时 x1、x2 的返回值都是 number。
function convert(x: P2): string;
function convert(x: P1): number;
function convert(x: P1 | P2): any { }
const x1 = convert({ name: '' } as P1); // => number
const x2 = convert({ name: '', age: 18 } as P2); // => string
而我们只需要将函数重载列表的顺序调换一下,类型为 P2 和 P1 的参数就可以分别匹配到正确的函数重载了,例如第 5 行匹配到第 2 行,第 6 行匹配到第 1 行。
所以,在定义重载的时候,一定要把最精确的定义放在最前面
类型谓词(is)
本人察觉这个有版本问题,后续再考虑
function isString(s): s is string { // 类型谓词
return typeof s === 'string';
}
“参数名 + is + 类型”的格式明确表明了参数的类型,进而引起类型缩小
类类型
他是将 继承、封装、多态三要素为一体的编程利器。
类
先看一个例子。
class Dog {
name: string
constructor(name: string) {
this.name = name
}
bark() {
console.log('Woof! Woof!')
}
}
const dog = new Dog('Q');
dog.bark()
首先,我们定义了一个 class Dog
,它拥有 string
类型的 name
属性(见第 2 行)、bark
方法(见第 7 行)和一个构造器函数(见第 3 行)。然后,我们通过 new
关键字创建了一个 Dog
的实例,并把实例赋值给变量 dog
(见 12 行)。最后,我们通过实例调用了类中定义的 bark
方法(见 13 行)。
继承
class Animal {
type = 'Animal';
say(name: string) {
console.log(`I'm ${name}`)
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!')
}
}
const dog = new Dog();
dog.bark();// Woof! Woof!
dog.say('SSS'); // I'm SSS
dog.type // Animal
上面的例子展示了类最基本的继承用法。比如第 8 ~12 行定义的Dog
是派生类,它派生自第 1~6 行定义的Animal
基类,此时Dog
实例继承了基类Animal
的属性和方法。因此,在第 15~17 行我们可以看到,实例 dog 支持 bark、say、type 等属性和方法。
这里的 Dog 基类与第一个例子中的类相比,少了一个构造函数。这是因为派生类如果包含一个构造函数,则必须在构造函数中调用 super() 方法,这是 TypeScript 强制执行的一条重要规则。
class Animal {
type = 'Animal';// 这一行翻译的时候会直接写进构造器里面
say(name: string) {
console.log(`I'm ${name}`)
}
}
class Dog extends Animal {
name: string;
constructor(name: string) {
super(); // 具体看这一行
this.name = name;
}
bark() {
console.log('Woof! Woof!')
}
}
有人可能会好奇,这里的 super() 是什么作用?其实这里的 super 函数会调用基类的构造函数,如下代码所示:
class Animal {
weight: number
type = 'Animal';
constructor(weight: number) {
this.weight = weight
}
say(name: string) {
console.log(`I'm ${name}`)
}
}
class Dog extends Animal {
name: string;
constructor(weight:number, name: string) {
super(weight); // 可以继承基类属性,也可以直接传数字就不会报错了
this.name = name;
}
bark() {
console.log('Woof! Woof!')
}
}
const dog = new Dog();
dog.bark();// Woof! Woof!
dog.say('SSS'); // I'm SSS
dog.type // Animal
公共、私有与受保护的修饰符
ts中支持3中访问修饰符,分别是public
、private
、protected
。
public
修饰的是任何地方可见,共有的属性或方法;pruvate
修饰的是仅在同一类中可见、私有的属性或方法;protected
修饰的是仅在类自身及子类中可见、受保护的属性或方法。
在之前的代码中,示例类并没有用到可见性修饰符,在缺省情况下,类的属性或方法默认都是 public
。如果想让有些属性对外不可见,那么我们可以使用private
进行设置,如下所示
class Son {
public firstName: string;
private lastName: string = 'Stark';
constructor(firstName: string) {
this.firstName = firstName
this.lastName
}
}
const son = new Son('Tony');
console.log(son.firstName); // => 'Tony'
son.firstName = 'Hansen'
console.log(son.firstName);
// Property 'lastName' is private and only accessible within class 'Son'.(2341)
console.log(son.lastName)
说明: 在上面的例子中我们可以看到,Son
类的 lastName
属性是私有的,只在 Son
类中可见 , 定义的 firstName
属性是公有的,在任何地方都可见。因此,我们既可以通过创建的 Son
类的实例 son
获取或设置公共的 firstName
的属性,还可以操作更改 firstName
的值。
不过,对于 private
修饰的私有属性,只可以在类的内部可见。比如私有属性 lastName
仅在 Son
类中可见,如果其他地方获取了 lastName
,TypeScript
就会提示一个 ts(2341)
的错误。
注意:TypeScript 中定义类的私有属性仅仅代表静态类型检测层面的私有。如果我们强制忽略 TypeScript 类型的检查错误,转译且运行 JavaScript 时依旧可以获取到 lastName 属性,这是因为 JavaScript 并不支持真正意义上的私有属性。
看转义过后的代码:
"use strict";
class Son {
constructor(firstName) {
this.lastName = 'Stark';
this.firstName = firstName;
this.lastName;
}
}
const son = new Son('Tony');
console.log(son.firstName); // => 'Tony'
son.firstName = 'Hansen';
console.log(son.firstName);
console.log(son.lastName);
下面来看下受保护的属性和方法
class Son {
public firstName: string;
protected lastName: string = 'Hansen';
constructor(firstName: string) {
this.firstName = firstName
this.lastName
}
}
class GrandSon extends Son {
constructor(firstName: string) {
super(firstName)
}
public getMyLastName() {
return this.lastName
}
}
const grandSon = new GrandSon('HUO JIN');
console.log(grandSon.getMyLastName()); // Hansen
// Property 'lastName' is protected and only accessible within class 'Son' and its subclasses.(2445)
grandSon.lastName
注意:虽然我们不能通过派生类的实例访问protected修饰的属性和方法,但是可以通过派生类的实例方法进行访问,通过实例的 getMyLastName 方法获取受保护的属性 lastName 是 ok 的,而通过实例直接获取受保护的属性 lastName 则提示了一个 ts(2445) 的错误。
只读修饰符
在前面的例子中,Son 类 public 修饰的属性既公开可见,又可以更改值,如果我们不希望类的属性被更改,则可以使用 readonly 只读修饰符声明类的属性,如下代码所示:
class Son {
public readonly firstName: string
constructor(firstName: string) {
this.firstName = firstName
}
}
const son = new Son('Hansen');
// Cannot assign to 'firstName' because it is a read-only property.(2540)
son.firstName = 'WSC'
我们给公开可见属性 firstName 指定了只读修饰符,这个时候如果再更改 firstName 属性的值,TypeScript 就会提示一个 ts(2540) 的错误,这是因为只读属性修饰符保证了该属性只能被读取,而不能被修改。
注意:如果只读修饰符和可见性修饰符同时出现,我们需要将只读修饰符写在可见修饰符后面。
存取器
在 TypeScript 中还可以通过getter、setter截取对类成员的读写访问。通过对类属性访问的截取,我们可以实现一些特定的访问控制逻辑。
class Son {
public firstName: string;
protected lastName: string = 'Hansen';
constructor(firstName: string) {
this.firstName = firstName;
}
}
class GrandSon extends Son {
constructor(firstName: string) {
super(firstName);
}
get myLastName() {
return this.lastName;
}
set myLastName(name: string) {
if(this.firstName === 'SJ') {
this.lastName = name;
} else {
console.error('Unab;e to change myLastName');
}
}
}
const grandSon = new GrandSon('SJ');
console.log(grandSon.myLastName);// Hansen
grandSon.myLastName = 'CC';
console.log(grandSon.myLastName);
const grandSon1 = new GrandSon('TK');
grandSon1.myLastName = 'CC';// "Unab;e to change myLastName"
只有在firstName
为SJ
的时候才不会走自己的错误。
静态属性
这些属性存在于类这个特殊的对象上,而不是类的实例上,所以我们可以直接通过类访问静态属性。
class MyArray {
static displayName = 'MyArray';
static isArray(obj: unknown) {
return Object.prototype.toString.call(obj).slice(8, -1) === 'Array';
}
}
console.log(MyArray.displayName); // 'MyArray'
console.log(MyArray.isArray([])); // true
console.log(MyArray.isArray({})); // false
通过 static
修饰符,我们给 MyArray
类分别定义了一个静态属性 displayName
和静态方法 isArray
。之后,我们无须实例化 MyArray
就可以直接访问类上的静态属性和方法了。
基于静态属性的特性,我们往往会把与类相关的常量、不依赖实例 this 上下文的属性和方法定义为静态属性,从而避免数据冗余,进而提升运行性能。
注意:上边我们提到了不依赖实例 this 上下文的方法就可以定义成静态方法,这就意味着需要显式注解 this 类型才可以在静态方法中使用 this;非静态方法则不需要显式注解 this 类型,因为 this 的指向默认是类的实例。(这边如果忘记了this,可以看看前面的this那一章节)
抽象类
它是一种不能被实例化仅能被子类继承的特殊类。我们可以使用抽象类定义派生类需要实现的属性和方法,同时也可以定义其他被继承的默认属性和方法。
看如下代码:
abstract class Adder {
abstract x: number;
abstract y: number;
abstract add(): number;
displayName = 'Adder'
addTwice(): number {
return (this.x + this.y) * 2
}
}
class NumAdder extends Adder {
x: number;
y: number;
constructor(x: number, y: number) {
super();
this.x = x;
this.y = y;
}
add(): number {
return this.x + this.y;
}
}
const numAdder = new NumAdder(1, 2);
console.log(numAdder.displayName); // => 'Adder'
console.log(numAdder.add()); // => 3
console.log(numAdder.addTwice()); // => 6
通过 abstract
关键字,我们定义了一个抽象类 Adder
,并通过abstract
关键字定义了抽象属性x、y
及方法add
,而且任何继承 Adder
的派生类都需要实现这些抽象属性和方法。同时,我们还在抽象类 Adder
中定义了可以被派生类继承的非抽象属性displayName
和方法addTwice
。
然后,我们定义了继承抽象类的派生类 NumAdder
, 并实现了抽象类里定义的 x、y
抽象属性和 add
抽象方法。如果派生类中缺少对 x、y、add
这三者中任意一个抽象成员的实现,那么就会提示一个 ts(2515) 错误。
说明:
- 抽象类中的其他非抽象成员则可以直接获取,如通过实例
numAdder
,我们获取了displayName
属性和addTwice
方法。 - 因为抽象类不能被实例化,并且派生类必须实现继承自抽象类上的抽象属性和方法定义,所以抽象类的作用其实就是对基础逻辑的封装和抽象。
- 还有,我们可以定义一个描述对象结构的接口类型抽象结构,并通过implements关键字约束实现。
抽象类与接口的区别
在于接口只能定义类成员的类。
interface IAdder {
x: number;
y: number;
add: () => number;
}
class NumAdder implements IAdder {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
add() {
return this.x + this.y;
}
addTwice() {
return (this.x + this.y) * 2;
}
}
类的类型
类的最后一个特性-----类的类型和函数类型,即在声明类的时候,其实也同时声明了一个特殊的类型(确切地讲是一个接口类型),这个类型的名字就是类型,表示类实例的类型;在定义类的时候,我们声明除构造函数外所有属性、方法的类型就是特殊类型的成员。
class A {
name: string;
constructor(name: string) {
this.name = name;
}
}
const a1: A = {}; // ts(2741) Property 'name' is missing in type '{}' but required in type 'A'.
const a2: A = { name: 'a2' }; // ok
我们在定义类 A
,说明我们同时定义了一个包含字符串属性 name
的同名接口类型 A
。因此,我们把一个空对象赋值给类型是 A
的变量 a1
时,TypeScript 会提示一个 ts(2741) 错误,因为缺少 name
属性。紧接着,我们把对象{ name: 'a2' }
赋值给类型同样是 A
的变量 a2
时,TypeScript 就直接通过了类型检查,因为有 name
属性。
接口类型与类型别名
Interface接口类型
TypeScript 对对象的类型检测遵循一种被称之为“鸭子类型”(duck typing)或者“结构化类型(structural subtyping)”的准则,即只要两个对象的结构一致,属性和方法的类型一致,则它们的类型就是一致的。如下代码:
function Study(language: { name: string; age: () => number }) {
console.log(`${language.name},${language.age()}`);
}
Study({
name: 'TypeScript',
age: () => new Date().getFullYear() - 1999
});
这上面虽然说是一种不错的定义方式,但是实际上,定义内联的接口类型是不可复用的,所以我们应该更多地使用interface
关键字来抽离可复用的接口类型。
来看下接口是怎么定义的吧。
/** 关键字 接口名称 */
interface ProgramLanguage {
// 语言名称
name: string;
// 使用年限
age: () => number
}
在前边示例中,通过内联参数类型定义的 Study
函数就可以直接使用 ProgramLanguage
接口来定义参数 language
的类型了。
function Study(language: ProgramLanguage) {
console.log(`${language.name},${language.age()}`);
}
我们还可以通过复用接口类型定义来约束其他逻辑。比如,我们通过如下所示代码定义了一个类型为 ProgramLanguage
的变量 TypeScript
。
let TypeScript: ProgramLanguage;
接着,我们把满足接口类型约定的一个对象字面量赋值给了这个变量,如下代码所示,此时也不会提示类型错误。
TypeScript = {
name: 'TypeScript',
age: () => new Date().getFullYear() - 2012
}
可缺省属性
在前边的例子中,如果我们希望缺少 age
属性的对象字面量也能符合约定且不抛出类型错误,确切地说在接口类型中 age
属性可缺省,那么我们可以在属性名之后通过添加如下所示的?
语法来标注可缺省的属性或方法。如以下示例中,OptionalProgramLanguage
接口就拥有一个可缺省的函数类型的 age
属性。
/** 关键字 接口名称 */
interface OptionalProgramLanguage {
/** 语言名称 */
name: string;
/** 使用年限 */
age?: () => number;
}
let OptionalTypeScript: OptionalProgramLanguage = {
name: 'TypeScript'
}; // ok
扩展: 我们看下如下代码
/** 关键字 接口名称 */
interface OptionalProgramLanguage2 {
/** 语言名称 */
name: string;
/** 使用年限 */
age: (() => number) | undefined;
}
说明哈,这里OptionalProgramLanguage2的age和上面OptionalProgramLanguage的age是不等价的,函数这章的可缺省参数和参数类型可以是 undefined 一样,可缺省意味着可以不设置属性键名,类型是 undefined 意味着属性键名不可缺省。不过可以加上类型守卫。代码如下:
if (typeof OptionalTypeScript.age === 'function') {
OptionalTypeScript.age();
}
OptionalTypeScript.age?.();
只读属性
对对象的某个属性或方法锁定写操作,这时,我们可以在属性名前面添加readon修饰符的语法来标注name为只读属性。
interface ReadonlyProgramLanguage {
// 语言名称
readonly name: string;
// 使用年限
readonly age: (() => number) | undefined
}
let ReadOnlyTypeScript: ReadonlyProgramLanguage = {
name: 'TypeScript',
age: undefined
}
/* Cannot assign to 'age' because it is a read-only property.(2540) */
ReadOnlyTypeScript.age = 2
需要注意的是,这仅仅是静态类型检测层面的只读,实际上并不能阻止对对象的篡改。因为在转译为 JavaScript
之后,readonly
修饰符会被抹除。因此,任何时候与其直接修改一个对象,不如返回一个新的对象,这会是一种比较安全的实践。
定义函数类型
备注:仅仅是定义函数的类型,而不包含函数的实现
interface StudyLanguage {
(language: ProgramLanguage): void
}
/** 单独的函数实践 */
let StudyInterface: StudyLanguage
= language => console.log(`${language.name} ${language.age()}`);
定义了一个接口类型 StudyLanguage,它有一个函数类型的匿名成员,函数参数类型 ProgramLanguage,返回值的类型是 void,通过这样的格式定义的接口类型又被称之为可执行类型,也就是一个函数类型。
索引签名
在实际工作中,使用接口类型较多的地方是对象,比如 React 组件的 Props & State、HTMLElement 的 Props,这些对象有一个共性,即所有的属性名、方法名都确定。
实际上,我们经常会把对象当 Map 映射使用,比如下边代码示例中定义了索引是任意数字的对象 LanguageRankMap 和索引是任意字符串的对象 LanguageMap。
let LanguageRankMap = {
1: 'TypeScript',
2: 'JavaScript',
...
};
let LanguageMap = {
TypeScript: 2012,
JavaScript: 1995,
...
};
这个时候,我们需要使用索引签名来定义上边提到的对象映射结构,并通过 “[索引名: 类型]”的格式约束索引的类型。
索引名称的类型分为 string 和 number 两种,通过如下定义的 LanguageRankInterface 和 LanguageYearInterface 两个接口,我们可以用来描述索引是任意数字或任意字符串的对象。
interface LanguageRankInterface {
[rank: number]: string;
}
interface LanguageYearInterface {
[name: string]: number;
}
{
let LanguageRankMap: LanguageRankInterface = {
1: 'TypeScript', // ok
2: 'JavaScript', // ok
'WrongINdex': '2012' // ts(2322) 不存在的属性名
};
let LanguageMap: LanguageYearInterface = {
TypeScript: 2012, // ok
JavaScript: 1995, // ok
1: 1970 // ok
};
}
注意:在上述示例中,数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0 或 ‘0’ 索引对象时,这两者等价。
继承与实现
在 TypeScript 中,接口类型可以继承和被继承,比如我们可以使用如下所示的 extends 关键字实现接口的继承。
interface ProgramLanguage{
a: string;
}
interface DynamicLanguage extends ProgramLanguage {
rank: number; // 定义新属性
}
interface TypeSafeLanguage extends ProgramLanguage {
typeChecker: string; // 定义新属性
}
// 继承多个
interface TypeScritLanguage extends DynamicLanguage, TypeSafeLanguage {
name: 'TypeScript' // 用原属性类型的兼容的类型(比如子集)重新定义属性
}
Type 类型别名
接口类型的一个作用是将内联类型抽离出来,从而实现类型可复用。其实,我们也可以使用类型别名接收抽离出来的内联类型实现复用。
此时,我们可以通过如下所示“type
别名名字 = 类型定义”的格式来定义类型别名。看代码。
/** 类型别名 */
{
type LanguageType = {
/** 以下是接口属性 */
/** 语言名称 */
name: string;
/** 使用年限 */
age: () => number;
}
}
在上述代码中,乍看上去有点像是在定义变量,只不过这里我们把 let 、const 、var 关键字换成了 type 罢了。
此外,针对接口类型无法覆盖的场景,比如组合类型、交叉类型,们只能使用类型别名来接收,如下代码所示:
{
// 联合类型
type MixedType = string | number;
// 交叉类型
type intersectionType = { id: number; name: string } & { age: number; name: string }
// 提取类型属性
type AgeType = ProgramLanguage['age']
}
注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
Interface 与 Type 的区别
在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。比如,重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展,如下代码:
{
interface Language {
id: number
}
interface Language {
name: string
}
let lang: Language = {
id: 2,
name: 'name'
}
}
在上述代码中,先后定义的两个 Language 接口属性被叠加在了一起,此时我们可以赋值给 lang 变量一个同时包含 id 和 name 属性的对象。
{
type Language = {
id: number
}
// Duplicate identifier 'Language'.(2300) 重复的标志
type Language = {
name: string
}
let lang: Language = {
id: 1,
name: 'name'
}
}
在上述代码中,我们重复定义了一个类型别名 Language ,此时就提示了一个错误。
联合类型和交叉类型
我们还需要通过组合/结合单一、原子类型构造更复杂的类型,以此描述更复杂的数据和结构。
联合类型
联合类型(Unions)用来表示变量、参数的类型不是单一原子类型,而可能是多种不同的类型的组合。
我们主要通过“|”操作符分隔类型的语法来表示联合类型。
下面代码,我们一个函数的参数可能是 number 或 string 的联合类型
function formatPX(size: number | string) {
}
formatPX(13); // ok
formatPX('13px'); // ok
formatPX(true);// rgument of type 'boolean' is not assignable to parameter of type 'string | number'.(2345)
formatPX(undefined)
我们定义了函数 formatPX
的参数 size
既可以是 number
类型也可以是 string
类型,所以传入数字 13
和字符串 '13px'
都正确,但是传入布尔类型的 true
或者 undefined
类型都会提示一个 ts(2345)
错误。
当然,我们可以组合任意个、任意类型来构造更满足我们诉求的类型。如下代码。
function formatUnit(size: number | string, unit: 'px' | 'em' | 'rem' | '%') {
}
formatUnit('1px', 'rem');
formatUnit(2, 'em')
formatUnit('1px', 'bem')// Argument of type '"bem"' is not assignable to parameter of type '"px" | "em" | "rem" | "%"'.(2345)
我们定义了 formatPX
函数的第二个参数 unit
,如果我们传入一个不在类型集合中的字符串字面量 'bem'
,就会提示一个 ts(2345)
错误。
我们也可以使用类型别名抽离上边的联合类型,然后再将其进一步地联合,如下代码所示:
type ModernUnit = 'vh' | 'vw';
type Unit = 'px' | 'em' | 'rem';
type MessedUp = ModernUnit | Unit; // 类型是 'vh' | 'vw' | 'px' | 'em' | 'rem'
我们也可以把接口类型联合起来表示更复杂的结构。(用下类型断言as)
interface Bird {
fly(): void;
layEggs(): void
}
interface Fish {
swim(): void;
layEggs(): void
}
const getPet: () => Bird | Fish = () => {
return {} as Bird | Fish
}
const Pet = getPet();
Pet.layEggs()
Pet.fly()// Property 'fly' does not exist on type 'Bird | Fish'.Property 'fly' does not exist on type 'Fish'.(2339)
在联合类型中,我们可以直接访问各个接口成员都拥有的属性、方法,且不会提示类型错误。但是,如果是个别成员特有的属性、方法,我们就需要区分对待了,此时又要引入类型守卫了。
只不过,在这种情况下,我们还需要使用基于 in 操作符判断的类型守卫,如下代码所示:
if (typeof Pet.fly === 'function') { // ts(2339)
Pet.fly(); // ts(2339)
}
if ('fly' in Pet) {
Pet.fly(); // ok
}
交叉类型
在 TypeScript 中,确实还存在一种类似逻辑与行为的类型——交叉类型(Intersection Type),它可以把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性。
在 TypeScript 中,我们可以使用“&”操作符来声明交叉类型,如下代码所示:
{
type Useless = string & number;
}
很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string
类型又是 number
类型。因此,在上述的代码中,类型别名 Useless
的类型就是个 never
。
合并接口类型
联合类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码
type IntersectionType = { id: number; name: string; } & { age: number }
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
在上述示例中,我们通过交叉类型,使得 IntersectionType
同时拥有了 id、name、age
所有属性,这里我们可以试着将合并接口类型理解为求并集。
这里,我们来发散思考一下:如果合并的多个接口类型存在同名属性会是什么效果呢?
解释一下
比如上面示例中两个接口类型同名的 name
属性类型一个是 number
,另一个是 string
,合并后,name
属性的类型就是 number
和 string
两个原子类型的交叉类型,即 never
。
type IntersectionTypeConfict = { id: number; name: string; }
& { age: number; name: number; };
const mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
age: 2
};
如果同名属性的类型兼容,比如一个是 number
,另一个是 number
的子类型、数字字面量类型,合并后 name
属性的类型就是两者中的子类型。
如下所示示例中 name
属性的类型就是数字字面量类型 2
,因此,我们不能把任何非 2
之外的值赋予 name
属性。
type IntersectionTypeConfict = { id: number; name: 2; }
& { age: number; name: number; };
let mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ok
age: 2
};
mixedConflict = {
id: 1,
name: 22, // '22' 类型不能赋给 '2' 类型
age: 2
};
合并联合类型
们可以合并联合类型为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员。这里,我们也可以将合并联合类型理解为求交集。
在如下示例中,两个联合类型交叉出来的类型 IntersectionUnion 其实等价于 ‘em’ | ‘rem’,所以我们只能把 ‘em’ 或者 ‘rem’ 字符串赋值给 IntersectionUnion 类型的变量。
type UnionA = 'px' | 'em' | 'rem' | '%';
type UnionB = 'vh' | 'em' | 'rem' | 'pt';
type IntersectionUnion = UnionA & UnionB;
const intersectionA: IntersectionUnion = 'em'; // ok
const intersectionB: IntersectionUnion = 'rem'; // ok
// Type '"px"' is not assignable to type '"em" | "rem"'.(2322)
const intersectionC: IntersectionUnion = 'px';
const intersectionD: IntersectionUnion = 'pt'; // ts(2322)
既然是求交集,如果多个联合类型中没有相同的类型成员,交叉出来的类型自然就是 never 了,如下代码所示:
type UnionC = 'em' | 'rem'
type UnionD = 'px' | 'pt'
type InterE = UnionC & UnionD
// Type 'any' is not assignable to type 'never'.(2322)
const interE: InterE = 'any' as any
因为 UnionC
和 UnionD
没有交集,交叉出来的类型 InterE
就是 never
,所以我们不能把任何类型的值赋予 InterE
类型的变量。
联合、交叉类型
在前面的示例中,我们把一些联合、交叉类型抽离成了类型别名,再把它作为原子类型进行进一步的联合、交叉。其实,联合、交叉类型本身就可以直接组合使用,这就涉及 |、& 操作符的优先级问题。实际上,联合、交叉运算符不仅在行为上表现一致,还在运算的优先级和 JavaScript 的逻辑或 ||、逻辑与 && 运算符上表现一致 。
联合操作符 | 的优先级低于交叉操作符 &,同样,我们可以通过使用小括弧 () 来调整操作符的优先级。
type UnionIntersectionA = { id: number; } & { name: string; } | { id: string; } & { name: number; }; // 交叉操作符优先级高于联合操作符
type UnionIntersectionB = ('px' | 'em' | 'rem' | '%') | ('vh' | 'em' | 'rem' | 'pt'); // 调整优先级
类型缩减
如果将 string 原始类型和“string字面量类型”组合成联合类型会是什么效果?效果就是类型缩减成 string 了。如下代码:
type URStr = 'string' | string; // 类型是 string
type URNum = 2 | number; // 类型是 number
type URBoolen = true | boolean; // 类型是 boolean
enum EnumUR {
ONE,
TWO
}
type URE = EnumUR.ONE | EnumUR; // 类型是 EnumUR
TypeScript 对这样的场景做了缩减,它把字面量类型、枚举成员类型缩减掉,只保留原始类型、枚举类型等父类型,这是合理的“优化”。
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // 类型缩减成 string
const borderColor: BorderColor = '' // 这里IDE并没有做提示,会落化提示功能,所以下面有好的办法。
如下代码所示,我们只需要给父类型添加“& {}”即可。
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // 字面类型都被保留
const borderColor: BorderColor = 'r'
此时,其他字面量类型就不会被缩减掉了,在 IDE 中字符串字面量 black、red 等也就自然地可以自动提示出来了。