安装node环境,新建项目文件夹
使用命令行安装ts版本
npm i -g [email protected].* tsc -V
tsconfig.json文件初始化
tsc --init;
修改tsconfig.json文件
{ "compilerOptions": { /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "strictNullChecks": true, /* Enable strict null checks. */ "strictFunctionTypes": true, /* Enable strict checking of function types. */ "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */ "outDir": "./js", //文件输出位置 }
创建HelloWorld.ts文件,编译后检查代码
function say(word: string) { console.log(word); say('Hello, World');
使用命令行设置严格模式和watch监听文件内容改变,实时进行类型检测和代码转译
tsc HelloWorld.ts --strict --alwaysStrict false --watch
转成js文件后再使用node执行js文件,就可以查看console.log
node HelloWorld.js
Typescript是Javascript的超集
可以在 TypeScript 中显式声明变量num仅仅是数字类型,也就是说只需在变量num后添加: number类型注解即可
特殊说明:number表示数字类型,:用来分割变量和类型的分隔符。
let num: number = 1;
string、number、boolean、bigint、undefined、symbol
(null 是一个伪原始类型,它在 JavaScript 中实际上是一个对象,且所有的结构化类型都是通过 null 原型链派生而来)
基础类型:字符串、数字(包括 number 和 bigint)、布尔值、Symbol ,
特殊字符:null 和 undefined 等。
let num:number =1 let str:string='str'; let boo:boolean=true; let nulls:null=null; let undefineds:undefined=undefined; let sym:symbol=Symbol('a'); //使用Symbol可能会报错,修改tsconfig.json文件"target": "es2016","lib": ["dom","es2016"],
当然,TypeScript 还包含 Number、String、Boolean、Symbol 等类型(注意区分大小写)。
它们和小写格式对应的 number、string、boolean、symbol 不等价
let sym: symbol = Symbol('a'); let sym2: Symbol = Symbol('b'); sym = sym2 //fail sym2 = sym // fail let str: String = new String('a'); let str2: string = 'a'; str = str2; // fail str2 = str; // fail
在编译时期,静态类型的编程语言即可准确地发现类型错误,这就是静态类型检测的优势。
在编译(转译)时期,TypeScript 编译器将通过对比检测变量接收值的类型与我们显示注解的类型,从而检测类型是否存在错误。如果两个类型完全一致,显示检测通过;如果两个类型不一致,它就会抛出一个编译期错误,告知我们编码错误
const trueNum: number = 42; const fakeNum: number = "42"; // ts(2322) Type 'string' is not assignable to type 'number'
使用[]定义数组类型(更优选择)
let arrayNumber:number[]=[1,2,4] let arrayString:string[]=['a','v']
使用Array泛型定义数组类型
let arrayOfNumber:Array<number>=[1,2,3,4] let arrayOfString:Array<string>=['c','v','b']
如果我们明确指定了数组元素的类型,以下所有操作都将因为不符合类型约定而提示错误
let arrayOfNumber: number[] = ['x', 'y', 'z']; // 提示 ts(2322) arrayOfNumber[3] = 'a'; // 提示 ts(2322) arrayOfNumber.push('b'); // 提示 ts(2345) let arrayOfString: string[] = [1, 2, 3]; // 提示 ts(2322) arrayOfString[3] = 1; // 提示 ts(2322) arrayOfString.push(2); // 提示 ts(2345)
元组中允许存储不同类型的元素,可以限制数组元素的个数和类型,适合用于多值返回。
any 指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式。
可以把任何类型的值赋值给 any 类型的变量,也可以把 any 类型的值赋值给任意类型(除 never 以外)的变量
所有 any 类型的任意属性的类型都是 any
因此,除非有充足的理由,否则我们应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。
let anything: any = {}; anything.doAnything(); // 不会提示错误 anything = 1; // 不会提示错误 anything = 'x'; // 不会提示错误 let nums2: number = anything; // 不会提示错误 let strs: string = anything;
unknown描述类型并不确定的变量
任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any,
如果不缩小类型(Type Narrowing),我们对 unknown 执行的任何操作都会出现如下所示错误
let result: unknown; result.toFixed(); // 提示 ts(2571)
而所有的类型缩小手段对 unknown 都有效,如下代码所示:
let result: unknown; if (typeof result === 'number') { result.toFixed(); // 此处 hover result 提示类型是 number,不会提示错误 }
void 类型:表示函数没有返回值
undefined 的最大价值主要体现在接口类型上,它表示一个可缺省、未定义的属性。可以把 undefined 值或类型是 undefined 的变量赋值给 void 类型变量,反过来,类型是 void 但值是 undefined 的变量不能赋值给 undefined 类型
let undeclared: undefined = undefined; // 鸡肋 let nullable: null = null; // 鸡肋
null 的价值主要体现在接口制定上,它表明对象或属性可能是空值
const userInfo: { name: null | string } = { name: null };
我们需要类型守卫(Type Guard,第 11 讲会专门讲解)在操作之前判断值的类型是否支持当前的操作。类型守卫既能通过类型缩小影响 TypeScript 的类型检测,也能保障 JavaScript 运行时的安全性
const userInfo: { id?: number; name?: null | string } = { id: 1, name: 'Captain' }; if (userInfo.id !== undefined) { // Type Guard userInfo.id.toFixed(); // id 的类型缩小成 number }
比非空断言更安全、类型守卫更方便的做法是使用单问号(Optional Chain)、双问号(空值合并),我们可以使用它们来保障代码的安全性
userInfo.id?.toFixed(); // Optional Chain const myName = userInfo.name?? `my name is ${info.name}`; //
表示永远不会产生值的类型
我们定义一个统一抛出错误的函数,代码示例如下(圆括号后 : + 类型注解 表示函数返回值的类型
因为永远不会有返回值,所以它的返回值类型就是 never
function ThrowError(msg: string): never { throw Error(msg); }
如果函数代码中是一个死循环,那么这个函数的返回值类型也是 never
function InfiniteLoop(): never { while (true) {} }
never 是所有类型的子类型,它可以给所有类型赋值
反过来,除了 never 自身以外,其他类型(包括 any 在内的类型)都不能为 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; // ts(2322)) props.name = 'str'; // ts(2322) props.name = 1; // ts(2322)
object 类型表示非原始类型的类型,即非 number、string、boolean、bigint、symbol、null、undefined 的类型
declare function create(o: object | null): any; create({}); // ok create(() => null); // ok create(2); // ts(2345) create('string'); // ts(2345)
类型断言(类似仅作用在类型层面的强制类型转换)
使用 as 语法做类型断言
const arraynumber:number[]=[1,2,3,4] const greaterThan2:number=arraynumber.find((number)=>number>2) as number
使用尖括号 + 类型的格式做类型断言
const arrayNumber: number[] = [1, 2, 3, 4]; const greaterThan2: number = <number>arrayNumber.find(num => num > 2);
any 和 unknown 这两个特殊类型属于万金油,因为它们既可以被断言成任何类型,反过来任何类型也都可以被断言成 any 或 unknown。因此,如果我们想强行“指鹿为马”,就可以先把“鹿”断言为 any 或 unknown,然后再把 any 和 unknown 断言为“马”,比如鹿 as any as 马。
使用“字面量值 + as const”语法结构进行常量断言
/** str 类型是 '"str"' */ let str = 'str' as const; /** readOnlyArr 类型是 'readonly [0, 1]' */ const readOnlyArr = [0, 1] as const;
特殊非空断言即在值(变量、属性)的后边添加 '!' 断言操作符,它可以用来排除值为 null、undefined 的情况对于非空断言来说,我们同样应该把它视作和 any 一样危险的选择。
let mayNullOrUndefinedOrString: null | undefined | string; mayNullOrUndefinedOrString!.toString(); // ok mayNullOrUndefinedOrString.toString(); // ts(2531)
建议使用类型守卫来代替非空断言,比如如下所示的条件判断
let mayNullOrUndefinedOrString: null | undefined | string; if (typeof mayNullOrUndefinedOrString === 'string') { mayNullOrUndefinedOrString.toString(); // ok }
在 TypeScript 中,类型标注声明是在变量之后(即类型后置)
let x1=42 let x1:number=42
在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。
在某些特定的情况下,我们也可以通过变量所在的上下文环境推断变量的类型
字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型
字面量类型是集合类型的子类型,它是集合类型的一种更具体的表达。比如 'this is string' (这里表示一个字符串字面量类型)类型是 string 类型(确切地说是 string 类型的子类型),而 string 类型不一定是 'this is string'(这里表示一个字符串字面量类型)类型
{ let specifiedStr: 'this is string' = 'this is string'; let str: string = 'any string'; specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string' str = specifiedStr; // ok }
定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合
type Direction = 'up' | 'down'; function move(dir: Direction) { // ... move('up'); // ok move('right'); // ts(2345
通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。
因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。
数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,
// 声明类型Config interface Config { size: 'small' | 'big'; isEnable: true | false; margin: 0 | 2 | 4; }
let 和 const 定义变量时变量类型不一致
缺省显式类型注解的可变更的变量的类型转换成赋值字面量类型的父类型
// 比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。 const str = 'this is string'; // str: 'this is string' const num = 1; // num: 1 const bool = true; // bool: true let str = 'this is string'; // str: string let num = 1; // num: number let bool = true; // bool: boolean }
TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
{ let str = 'this is string'; // 类型是 string let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string; const specifiedStr = 'this is string'; // 类型是 'this is string' let str2 = specifiedStr; // 类型是 'string' let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string; }
基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。
{ const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"' let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string' }
对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any;
{ let x = null; // 类型拓宽成 any let y = undefined; // 类型拓宽成 any /** -----分界线------- */ const z = null; // 类型是 null /** -----分界线------- */ let anyFun = (param = null) => param; // 形参类型是 null let z2 = z; // 类型是 null let x2 = x; // 类型是 null let y2 = y; // 类型是 undefined // 示例第 7~10 行的类型推断行为因为开启了 strictNullChecks=true(说明:本课程所有示例都基于严格模式编写)
在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。
可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型。
{ let func = (anything: any) => { if (typeof anything === 'string') { return anything; // 类型是 string } else if (typeof anything === 'number') { return anything; // 类型是 number return null; }
可以使用类型守卫将联合类型缩小到明确的子类型
{ let func = (anything: string | number) => { if (typeof anything === 'string') { return anything; // 类型是 string } else { return anything; // 类型是 number }
可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型
{ type Goods = 'pen' | 'pencil' |'ruler'; const getPenCost = (item: 'pen') => 2; const getPencilCost = (item: 'pencil') => 4; const getRulerCost = (item: 'ruler') => 6; const getCost = (item: Goods) => { if (item === 'pen') { return getPenCost(item); // item => 'pen' } else if (item === 'pencil') { return getPencilCost(item); // item => 'pencil' } else { return getRulerCost(item); // item => 'ruler' }
在 TypeScript 中,如果我们显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒
function fn(): undefined { // ts(2355) A function whose declared type is neither 'void' nor 'any' must return a value // TODO }
应该使用void表示没有返回值的类型
function fn1(): void { fn1().doSomething(); // ts(2339) Property 'doSomething' does not exist on type 'void'.
可以使用类似定义箭头函数的语法来表示函数类型的参数和返回值类型,此时=> 类型仅仅用来定义一个函数类型而不用实现这个函数。
右侧的箭头函数并没有显式声明类型注解,不过可以根据上下文类型进行推断。
type Adder = (a: number, b: number) => number; // TypeScript 函数类型定义 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; };
从类型层面看,我们也可以通过类型推断(回想一下类型推断、上下文类型推断)加工计算入参的类型,并返回新的类型
function computeTypes(one: string, two: number) { const nums = [two]; const strs = [one] return { nums, } // 返回 { nums: number[]; strs: string[] } 的类型 }
函数返回值的类型推断结合泛型可以实现特别复杂的类型计算(本质是复杂的类型推断,这里称之为计算是为了表明其复杂性),比如 Redux Model 中 State、Reducer、Effect 类型的关联。
Generator 函数返回的是一个 Iterator 迭代器对象,我们可以使用 Generator 的同名接口泛型或者 Iterator 的同名接口泛型(在 10 讲会介绍)表示返回值的类型(Generator 类型继承了 Iterator 类型),
type AnyType = boolean; type AnyReturnType = string; type AnyNextType = number; function *gen(): Generator<AnyType, AnyReturnType, AnyNextType> { const nextValue = yield true; // nextValue 类型是 number,yield 后必须是 boolean 类型 return `${nextValue}`; // 必须返回 string 类型 }
在类型标注的:前添加?表示 log 函数的参数 x 就是可缺省的。
function log(x?: string) { return x; log(); // => undefined log('hello world'); // => hello world
?: 表示参数可以缺省、可以不传,也就是说调用函数时,我们可以不显式传入参数。但是,如果我们声明了参数类型为 xxx | undefined(这里使用了联合类型 |,详见 08 讲),就表示函数参数是不可缺省且类型必须是 xxx 或者 undfined。
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);
因此,在上述代码中,log1 函数如果不显示传入函数的参数,TypeScript 就会报一个 ts(2554) 的错误,即函数需要 1 个参数,但是我们只传入了 0 个参数。
TypeScript 会根据函数的默认参数的类型来推断函数参数的类型
function log(x = 'hello') { console.log(x); log(); // => 'hello' log('hi'); // => 'hi' log(1); // ts(2345) Argument of type '1' is not assignable to parameter of type 'string | undefined // 根据函数的默认参数 'hello' ,TypeScript 推断出了 x 的类型为 string | undefined。
对于默认参数,TypeScript 也可以显式声明参数的类型(一般默认参数的类型是参数类型的子集时,我们才需要这么做)。
不过,此时的默认参数只起到参数默认值的作用,
/** * 显式声明了函数参数 x 的类型为 number,表示函数参数 x 的类型可以不传或者是 number 类型。因此,如果我们将默认值设置为字符串类型,编译器就会抛出一个 ts(2322) 的错误。 function log1(x: string = 'hello') { console.log(x); // ts(2322) Type 'string' is not assignable to type 'number' function log2(x: number = 'hello') { console.log(x); log2(); log2(1); log2('1');
函数的默认参数类型必须是参数类型的子类型
/** 函数 log3 的函数参数 x 的类型为可选的联合类型 number | string,但是因为默认参数字符串类型是联合类型 number | string 的子类型,所以 TypeScript 也会检查通过。 function log3(x: number | string = 'hello') { console.log(x); }
在 ES6 中,JavaScript 支持函数参数的剩余参数,它可以把多个参数收集到一个变量中。同样,在TypeScript 中也支持这样的参数类型定义
// number[],表示所有被求和的参数是数字类型 function sum(...nums: number[]) { return nums.reduce((a, b) => a + b, 0); sum(1, 2); // => 3 sum(1, 2, 3); // => 6 sum(1, '2'); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number
如果将函数参数 nums 聚合的类型定义为 (number | string)[]
function sum(...nums: (number | string)[]): number { return nums.reduce<number>((a, b) => a + Number(b), 0); sum(1, '2', 3); // 6
通过指定 this 的类型(严格模式下,必须显式指定 this 的类型),当我们错误使用了 this,TypeScript 就会提示我们
function say() { console.log(this.name); // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation say();
在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,
比如最简单的作为对象的方法的 this 指向,
function say(this: Window, name: string) { console.log(this.name); window.say = say; window.say('hi'); const obj = { obj.say('hi'); // ts(2684) The 'this' context of type '{ say: (this: Window, name: string) => void; }' is not assignable to method's 'this' of type 'Window'
同样,定义对象的函数属性时,只要实际调用中 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; fn(); // ts(2684)
可以显式限定类(class 类)函数属性中的 this 类型,TypeScript 也能检查出错误的使用方式
class Component { onClick(this: Component) {} const component = new Component(); interface UI { addClickListener(onClick: (this: void) => void): void; const ui: UI = { addClickListener() {} ui.addClickListener(new Component().onClick); // ts(2345) new Component的this是Component,onClick的this是void ,两者不一样,所以报错
在链式调用风格的库中,使用 this 也可以很方便地表达出其类型
class Container { private val: number; constructor(val: number) { this.val = val; 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 * 因为 Container 类中 map、log 等函数属性(方法)未显式指定 this 类型,默认类型是 Container, * 所以以上方法在被调用时返回的类型也是 Container,this 指向一直是类的实例,它可以一直无限地被链式调用。 */
JavaScript 是一门动态语言,针对同一个函数,它可以有多种不同类型的参数与返回值,这就是函数的多态。
而在 TypeScript 中,也可以相应地表达不同类型的参数和返回值的函数
// 1~3 行定义了三种各不相同的函数类型列表,并描述了不同的参数类型对应不同的返回值类型, // 从第 4 行开始才是函数的实现。 * 注意:函数重载列表的各个成员(即示例中的 1 ~ 3 行)必须是函数实现(即示例中的第 4 行)的子集, “function convert(x: string): number”是“function convert(x: string | number | null): any”的子集。 * 在 convert 函数被调用时,TypeScript 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义。因此,我们需要把最精确的函数重载放到前面。 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
在添加返回值类型的地方,我们通过“参数名 + is + 类型”的格式明确表明了参数的类型,进而引起类型缩小,所以类型谓词函数的一个重要的应用场景是实现自定义类型守卫
function isString(s): s is string { // 类型谓词 return typeof s === 'string'; function isNumber(n: number) { return typeof n === 'number'; function operator(x: unknown) { if(isString(x)) { // ok x 类型缩小为 string if (isNumber(x)) { // ts(2345) unknown 不能赋值给 number }
ts的class类声明
class Dog { name: string; constructor(name: string) { this.name = name; bark() { console.log('Woof! Woof!'); const dog = new Dog('Q'); dog.bark(); // => 'Woof! Woof!'
在 TypeScript 中,使用 extends 关键字就能很方便地定义类继承的抽象模式
派生类如果包含一个构造函数,则必须在构造函数中调用 super() 方法,这是 TypeScript 强制执行的一条重要规则。
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('Q'); // => I'm Q! dog.type; // => Animal
类属性和方法除了可以通过 extends 被继承之外,还可以通过修饰符控制可访问性。
在 TypeScript 中就支持 3 种访问修饰符,分别是 public、private、protected。
public 修饰的是在任何地方可见、公有的属性或方法;
private 修饰的是仅在同一类中可见、私有的属性或方法;
protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。
在之前的代码中,示例类并没有用到可见性修饰符,在缺省情况下,类的属性或方法默认都是 public。如果想让有些属性对外不可见,那么我们可以使用private进行设置.
对于 private 修饰的私有属性,只可以在类的内部可见。私有属性 lastName 仅在 Son 类中可见,如果其他地方获取了 lastName ,TypeScript 就会提示一个 ts(2341) 的错误
class Son { public firstName: string; private lastName: string = 'Stark'; constructor(firstName: string) { this.firstName = firstName; this.lastName; // ok const son = new Son('Tony'); console.log(son.firstName); // => "Tony" son.firstName = 'Jack'; console.log(son.firstName); // => "Jack" console.log(son.lastName); // ts(2341) Property
如果不希望类的属性被更改,则可以使用 readonly 只读修饰符声明类的属性
class Son { public readonly firstName: string; constructor(firstName: string) { this.firstName = firstName; const son = new Son('Tony'); son.firstName = 'Jack'; // ts(2540)
在 TypeScript 中还可以通过getter、setter截取对类成员的读写访问。
通过对类属性访问的截取,我们可以实现一些特定的访问控制逻辑。
class Son { public firstName: string; protected lastName: string = 'Stark'; 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 === 'Tony') { this.lastName = name; } else { console.error('Unable to change myLastName'); }
关于类的所有属性和方法,只有类在实例化时才会被初始化。实际上,我们也可以给类定义静态属性和方法
静态属性和方法存在于类这个特殊的对象上,而不是类的实例上,所以我们可以直接通过类访问静态属性
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
基于静态属性的特性,我们往往会把与类相关的常量、不依赖实例 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
可以定义一个描述对象结构的接口类型抽象类的结构,并通过 implements 关键字约束类的实现。
使用接口与使用抽象类相比,区别在于接口只能定义类成员的类型,
定义了一个包含 x、y、add 属性和方法的接口类型,然后实现了拥有接口约定的x、y 属性和 add 方法,以及接口未约定的 addTwice 方法的NumAdder类
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 属性
TypeScript 对对象的类型检测遵循一种被称之为“鸭子类型”(duck typing)或者“结构化类型(structural subtyping)”的准则,即只要两个对象的结构一致,属性和方法的类型一致,则它们的类型就是一致的。
定义了一个拥有 string 类型属性name、函数类型属性age的对象 language 作为参数(形参 Parameter)的函数。同时,我们还使用类似定义 JavaScript 对象字面量的语法定义了一个内联接口类型来约束参数对象的类型。
function Study(language: { name: string; age: () => number }) { console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`); Study({ name: 'TypeScript', age: () => new Date().getFullYear() - 2012 });
如果我们传入一个包含了形参类型定义里没有的 id 属性的对象字面量作为实参,也会得到一个类型错误 ts(2345),实参(Argument)与形参(Parameter)类型不兼容,不存在的属性 id
/** ts(2345) 实参(Argument)与形参(Parameter)类型不兼容,不存在的属性 id */ Study({ id: 2, name: 'TypeScript', age: () => new Date().getFullYear() - 2012 });
如果我们先把这个对象字面量赋值给一个变量,然后再把变量传递给函数进行调用,那么 TypeScript 静态类型检测就会仅仅检测形参类型中定义过的属性类型,而包容地忽略任何多余的属性,此时也不会抛出一个 ts(2345) 类型错误。
这并非一个疏忽或 bug,而是有意为之地将对象字面量和变量进行区别对待,我们把这种情况称之为对象字面量的 freshness
let ts = { id: 2, name: 'TypeScript', age: () => new Date().getFullYear() - 2012 Study(ts); // ok
因为这种内联形式的接口类型定义在语法层面与熟知的 JavaScript 解构颇为神似,所以很容易让我们产生混淆。下面我们通过如下示例对比一下解构语法与内联接口类型混用的效果。
/** 纯 JavaScript 解构语法 */ function StudyJavaScript({name, age}) { console.log(name, age); /** TypeScript 里解构与内联类型混用 */ function StudyTypeScript({name, age}: {name: string, age: () => number}) { console.log(name, age); /** 纯 JavaScript 解构语法,定义别名 */ function StudyJavaScript({name: aliasName}) { // 定义name的别名 console.log(aliasName); /** TypeScript */ function StudyTypeScript(language: {name: string}) { // console.log(name); // 不能直接打印name console.log(language.name); }
接口的语法格式是在 interface 关键字的空格后+接口名字,然后属性与属性类型的定义用"{}"包裹。
/ ** 关键字 接口名称 */ interface ProgramLanguage { /** 语言名称 */ name: string; /** 使用年限 */ age: () => number; // 通过内联参数类型定义的 Study 函数就可以直接使用 ProgramLanguage 接口来定义参数 language 的类型了 function NewStudy(language: ProgramLanguage) { console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`); }
还可以通过复用接口类型定义来约束其他逻辑
// 定义了一个类型为 ProgramLanguage 的变量 TypeScript 。 let TypeScript: ProgramLanguage; // 接着,把满足接口类型约定的一个对象字面量赋值给了这个变量,此时也不会提示类型错误。 TypeScript = { name: 'TypeScript', age: () => new Date().getFullYear() - 2012 // 而任何不符合约定的情况,都会提示类型错误。
在接口类型中 age 属性可缺省,那么我们可以在属性名之后通过添加如下所示的**?** 语法来标注可缺省的属性或方法
当属性被标注为可缺省后,它的类型就变成了显式指定的类型与 undefined 类型组成的联合类型
/** 关键字 接口名称 */ interface OptionalProgramLanguage { /** 语言名称 */ name: string; /** 使用年限 */ age?: () => number; //(() => number) | undefined; let OptionalTypeScript: OptionalProgramLanguage = { name: 'TypeScript' }; // ok
我们可能还会碰到这样的场景,希望对对象的某个属性或方法锁定写操作,可以在属性名前通过添加 readonly 修饰符的语法来标注 某属性 为只读属性。
interface ReadOnlyProgramLanguage { /** 语言名称 */ readonly name: string; /** 使用年限 */ readonly age: (() => number) | undefined; let ReadOnlyTypeScript: ReadOnlyProgramLanguage = { name: 'TypeScript', age: undefined /** ts(2540)错误,name 只读 */ ReadOnlyTypeScript.name = 'JavaScript';
需要注意的是,这仅仅是静态类型检测层面的只读,实际上并不能阻止对对象的篡改。因为在转译为 JavaScript 之后,readonly 修饰符会被抹除。因此,任何时候与其直接修改一个对象,不如返回一个新的对象,这会是一种比较安全的实践。
我们定义了一个接口类型 StudyLanguage,它有一个函数类型的匿名成员,函数参数类型 ProgramLanguage,返回值的类型是 void,通过这样的格式定义的接口类型又被称之为可执行类型,也就是一个函数类型。
interface StudyLanguage { (language: ProgramLanguage): void /** 单独的函数实践 */ let StudyInterface: StudyLanguage = language => console.log(`${language.name} ${language.age()}`);
实际上,我们很少使用接口类型来定义函数的类型,更多使用内联类型或类型别名(本讲后半部分讲解)配合箭头函数语法来定义函数类型
// 我们给箭头函数类型指定了一个别名 StudyLanguageType,在其他地方就可以直接复用 StudyLanguageType,而不用重新声明新的箭头函数类型定义。 type StudyLanguageType = (language: ProgramLanguage) => void
在实际工作中,使用接口类型较多的地方是对象,比如 React 组件的 Props & State、HTMLElement 的 Props,这些对象有一个共性,即所有的属性名、方法名都确定。
索引名称的类型分为 string 和 number 两种,并通过 “[索引名: 类型]”的格式约束索引的类型。
通过如下定义的 LanguageRankInterface 和 LanguageYearInterface 两个接口,我们可以用来描述索引是任意数字或任意字符串的对象。
注意:在示例中,数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0 或 '0' 索引对象时,这两者等价。
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 }
可以使用 readonly 注解索引签名,此时将对应属性设置为只读就行
// LanguageRankInterface 和 LanguageYearInterface 任意的数字或者字符串类型的属性都是只读的。 interface LanguageRankInterface { readonly [rank: number]: string; interface LanguageYearInterface { readonly [name: string]: number; }
虽然属性可以与索引签名进行混用,但是属性的类型必须是对应的数字索引或字符串索引的类型的子集,否则会出现错误提示。
{ interface StringMap { [prop: string]: number; age: number; // ok name: string; // ts(2411) name 属性的 string 类型不能赋值给字符串索引类型 number interface NumberMap { [rank: number]: string; 1: string; // ok 0: number; // ts(2412) 0 属性的 number 类型不能赋值给数字索引类型 string interface LanguageRankInterface { name: string; // ok 0: number; // ok [rank: number]: string; [name: string]: number; }
在上述示例中,因为接口 StringMap 属性 name 的类型 string 不是它所对应的字符串索引(第 3 行定义的 prop: string)类型 number 的子集,所以会提示一个错误。同理,因为接口 NumberMap 属性 0 的类型 number 不是它所对应的数字索引(第 8 行定义的 rank: number)类型 string 的子集,所以也会提示一个错误。
另外,由于上边提到了数字类型索引的特殊性,所以我们不能约束数字索引属性与字符串索引属性拥有截然不同的类型,
{ interface LanguageRankInterface { [rank: number]: string; // ts(2413) 数字索引类型 string 类型不能赋值给字符串索引类型 number [prop: string]: number; }
这里我们定义了 LanguageRankInterface 的数字索引 rank 的类型是 string,与定义的字符串索引 prop 的类型 number 不兼容,所以会提示一个 ts(2413) 错误。
在 TypeScript 中,接口类型可以继承和被继承,比如我们可以使用如下所示的 extends 关键字实现接口的继承。
定义了同时继承了 DynamicLanguage 和 TypeSafeLanguage 的接口 TypeScriptLanguage,它会继承 DynamicLanguage 和 TypeSafeLanguage 所有的属性定义,并且使用同名的 name 属性定义覆盖了继承过来的 name 属性定义。
{ interface DynamicLanguage extends ProgramLanguage { rank: number; // 定义新属性 interface TypeSafeLanguage extends ProgramLanguage { typeChecker: string; // 定义新的属性 /** 继承多个 */ interface TypeScriptLanguage extends DynamicLanguage, TypeSafeLanguage { name: 'TypeScript'; // 用原属性类型的兼容的类型(比如子集)重新定义属性 }
我们仅能使用兼容的类型覆盖继承的属性
{ /** 因为 ProgramLanguage 的 name 属性是 string 类型,WrongTypeLanguage 的 name 属性是 number,二者不兼容,所以不能继承,也会提示一个 ts(6196) 错误 /** ts(6196) 错误的继承,name 属性不兼容 */ interface WrongTypeLanguage extends ProgramLanguage { name: number; }
我们既可以使用接口类型来约束类,反过来也可以使用类实现接口,那两者之间的关系到底是什么呢?这里,我们通过使用如下所示的 implements 关键字描述一下类和接口之间的关系。
在代码中,类 LanguageClass 实现了 ProgramLanguage 接口约定的 name、age 等属性和方法,如果我们移除 name 或者 age 的实现,将会提示一个类型错误。
/** 类实现接口 */ class LanguageClass implements ProgramLanguage { name: string = ''; age = () => new Date().getFullYear() - 2012 }
接口类型的一个作用是将内联类型抽离出来,从而实现类型可复用。其实,我们也可以使用类型别名接收抽离出来的内联类型实现复用。
此时,我们可以通过如下所示“type别名名字 = 类型定义”的格式来定义类型别名。
/** 类型别名 */ type LanguageType = { /** 以下是接口属性 */ /** 语言名称 */ name: string; /** 使用年限 */ age: () => number; }
此外,针对接口类型无法覆盖的场景,比如组合类型、交叉类型,我们只能使用类型别名来接收,
{ /** 联合 */ type MixedType = string | number; /** 交叉 */ type IntersectionType = { id: number; name: string; } & { age: number; name: string }; /** 提取接口属性类型 */ type AgeType = ProgramLanguage['age']; }
在上述代码中,我们定义了一个 IntersectionType 类型别名,表示两个匿名接口类型交叉出的类型;同时定义了一个 AgeType 类型别名,表示抽取的 ProgramLanguage age 属性的类型。
类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。比如,重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展.
{ interface Language { id: number; interface Language { name: string; let lang: Language = { id: 1, // ok name: 'name' // ok }
在上述代码中,先后定义的两个 Language 接口属性被叠加在了一起,此时我们可以赋值给 lang 变量一个同时包含 id 和 name 属性的对象。
不过,如果我们重复定义类型别名,如下代码所示,则会提示一个 ts(2300) 错误。
{ /** ts(2300) 重复的标志 */ type Language = { id: number; /** ts(2300) 重复的标志 */ type Language = { name: string; let lang: Language = { id: 1, name: 'name' }
在上述代码中,我们重复定义了一个类型别名 Language ,此时就提示了一个错误
联合类型(Unions)用来表示变量、参数的类型不是单一原子类型,而可能是多种不同的类型的组合。
我们主要通过“|”操作符分隔类型的语法来表示联合类型。这里,我们可以把“|”类比为 JavaScript 中的逻辑或 “||”,只不过前者表示可能的类型。
说明:在学习联合类型之前,我们可能免不了使用 any 或 unknown 类型来表示参数的类型(为了让大家养成好习惯,推荐使用 unknown)。
function formatPX(size: unknown) { if (typeof size === 'number') { return `${size}px`; if (typeof size === 'string') { return `${parseInt(size) || 0}px`; throw Error(` 仅支持 number 或者 string`); formatPX(13); formatPX('13px');
在有联合类型下,可以使用更明确的表达方式
function formatPX(size: number | string) { // ... formatPX(13); // ok formatPX('13px'); // ok formatPX(true); // ts(2345) 'true' 类型不能赋予 'number | string' 类型 formatPX(null); // ts(2345) 'null' 类型不能赋予 'number | string' 类型
也可以组合任意个、任意类型来构造更满足我们诉求的类型。
比如,我们希望给前边的示例再加一个 unit 参数表示可能单位,这个时候就可以声明一个字符串字面类型组成的联合类型,
function formatUnit(size: number | string, unit: 'px' | 'em' | 'rem' | '%' = 'px') { // ... formatUnit(1, 'em'); // ok formatUnit('1px', 'rem'); // ok formatUnit('1px', 'bem'); // ts(2345)
也可以使用类型别名抽离上边的联合类型,然后再将其进一步地联合
type ModernUnit = 'vh' | 'vw'; type Unit = 'px' | 'em' | 'rem'; type MessedUp = ModernUnit | Unit; // 类型是 'vh' | 'vw' | 'px' | 'em' | 'rem'
也可以把接口类型联合起来表示更复杂的结构
// 在联合类型中,我们可以直接访问各个接口成员都拥有的属性、方法,且不会提示类型错误。 // 但是,如果是个别成员特有的属性、方法,我们就需要区分对待了,此时又要引入类型守卫来区分不同的成员类型。 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(); // ok Pet.fly(); // ts(2339) 'Fish' 没有 'fly' 属性; 'Bird | Fish' 没有 'fly' 属性
还需要使用基于 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; //never }
联合类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,可以试着将合并接口类型理解为求并集。
type IntersectionType = { id: number; name: string; } & { age: number }; const mixed: IntersectionType = { id: 1, name: 'name', age: 18 }
合并的多个接口类型存在同名属性的效果
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的子类型、数字字面量类型,合并后的属性类型就是子类型
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 const intersectionC: IntersectionUnion = 'px'; // ts(2322) const intersectionD: IntersectionUnion = 'pt'; // ts(2322)
既然是求交集,如果多个联合类型中没有相同的类型成员,交叉出来的类型自然就是 never 了,所以不能把任何类型的值赋予类型的变量.
type UnionC = 'em' | 'rem'; type UnionD = 'px' | 'pt'; type IntersectionUnionE = UnionC & UnionD; const intersectionE: IntersectionUnionE = 'any' as any; // ts(2322) 不能赋予 'never' 类型
在前面,把一些联合、交叉类型抽离成了类型别名,再把它作为原子类型进行进一步的联合、交叉。其实,联合、交叉类型本身就可以直接组合使用,这就涉及 |、& 操作符的优先级问题。实际上,联合、交叉运算符不仅在行为上表现一致,还在运算的优先级和 JavaScript 的逻辑或 ||、逻辑与 && 运算符上表现一致 。
联合操作符 | 的优先级低于交叉操作符 &,同样,我们可以通过使用小括弧 () 来调整操作符的优先级。
type UnionIntersectionA = { id: number; } & { name: string; } | { id: string; } & { name: number; }; // 交叉操作符优先级高于联合操作符 type UnionIntersectionB = ('px' | 'em' | 'rem' | '%') | ('vh' | 'em' | 'rem' | 'pt'); // 调整优先级
进而,也可以把分配率、交换律等基本规则引入类型组合中,然后优化出更简洁、清晰的类型.
type UnionIntersectionC = ({ id: number; } & { name: string; } | { id: string; }) & { name: number; }; type UnionIntersectionD = { id: number; } & { name: string; } & { name: number; } | { id: string; } & { name: number; }; // 满足分配率 type UnionIntersectionE = ({ id: string; } | { id: number; } & { name: string; }) & { name: number; }; // 满足交换律
如果将 string 原始类型和“string字面量类型”组合成联合类型会是什么效果?效果就是类型缩减成 string 了。
同样,对于 number、boolean(还有枚举类型)也是一样的缩减逻辑.
type URStr = 'string' | string; // 类型是 string type URNum = 2 | number; // 类型是 number type URBoolen = true | boolean; // 类型是 boolean enum EnumUR { type URE = EnumUR.ONE | EnumUR; // 类型是 EnumUR
TypeScript 对这样的场景做了缩减,它把字面量类型、枚举成员类型缩减掉,只保留原始类型、枚举类型等父类型,这是合理的“优化”。
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string;
TypeScript 官方其实还提供了一个黑魔法,它可以让类型缩减被控制。我们只需要给父类型添加“& {}”即可。
此时,其他字面量类型就不会被缩减掉了,在 IDE 中字符串字面量 black、red 等也就自然地可以自动提示出来了。
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {};
当联合类型的成员是接口类型,如果满足其中一个接口的属性是另外一个接口属性的子集,这个属性也会类型缩减
// 这里因为 '1' 是 '1' | '2' 的子集,所以 age 的属性变成 '1' | '2': type UnionInterce = age: '1'; age: '1' | '2'; [key: string]: string; });
课后题
如何定义如下所示 age 属性是数字类型,而其他不确定的属性是字符串类型的数据结构的对象?
// 用到两个接口的联合类型及类型缩减,这个问题的核心在于找到一个既是 number 的子类型,这样 age 类型缩减之后的类型就是 number;同时也是 string 的子类型,这样才能满足属性和 string 索引类型的约束关系。 // never 有一个特性是它是所有类型的子类型,自然也是 number 和 string 的子类型。 type UnionInterce = age: number; age: never; [key: string]: string; const O: UnionInterce = { age: 2, string: 'string' };
枚举(Enums),用来表示一个被命名的整型常数的集合
在 TypeScript 中,我们可以使用枚举定义包含被命名的常量的集合,比如 TypeScript 支持数字、字符两种常量值的枚举类型。
我们也可以使用 enum 关键字定义枚举类型,格式是 enum + 枚举名字 + 一对花括弧,花括弧里则是被命名了的常量成员。
相对于其他类型,enum 也是一种比较特殊的类型,因为它兼具值和类型于一体,有点类似 class(在定义 class 结构时, 其实我们也自动定义了 class 实例的类型)。
7 种常见的枚举类型:数字类型、字符串类型、异构类型、常量成员和计算(值)成员、枚举成员类型和联合枚举、常量枚举、外部枚举。
enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY // Day 既可以表示集合,也可以表示集合的类型,所有成员(enum member)的类型都是 Day 的子类型。
仅仅指定常量命名的情况下,我们定义的就是一个默认从 0 开始递增的数字集合,称之为数字枚举。
如果我们希望枚举值从其他值开始递增,则可以通过“常量命名 = 数值” 的格式显示指定枚举成员的初始值
enum Day { SUNDAY = 1, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }
常量命名、结构顺序都一致的两个枚举,即便转译为 JavaScript 后,同名成员的值仍然一样(满足恒等 === )。但在 TypeScript 看来,它们不相同、不满足恒等.
enum MyDay { SUNDAY, Day.SUNDAY === MyDay.SUNDAY; // ts(2367) 两个枚举值恒不相等 work(MyDay.SUNDAY); // ts(2345) 'MyDay.SUNDAY' 不能赋予 'Day'
不仅仅是数字类型枚举,所有其他枚举都仅和自身兼容,这就消除了由于枚举不稳定性可能造成的风险,所以这是一种极其安全的设计。不过,这可能会使得枚举变得不那么好用,因为不同枚举之间完全不兼容,所以不少 TypeScript 编程人员觉得枚举类型是一种十分鸡肋的类型。而两个结构完全一样的枚举类型如果互相兼容,则会更符合我们的预期,比如说基于 Swagger 自动生成的不同模块中结构相同且描述同一个常量集合的多个同名枚举。
不过,此时我们可能不得不使用类型断言(as)或者重构代码将“相同“的枚举类型抽离为同一个公共的枚举(我们更推荐后者)
在 TypeScript 中,我们将定义值是字符串字面量的枚举称之为字符串枚举,字符串枚举转译为 JavaScript 之后也将保持这些值
相比于没有明确意义的递增值的数字枚举,字符串枚举的成员在运行和调试阶段,更具备明确的含义和可读性,枚举成员的值就是我们显式指定的字符串字面量。
enum Day { SUNDAY = 'SUNDAY', MONDAY = 'MONDAY', }
TypeScript 支持枚举类型同时拥有数字和字符类型的成员,这样的枚举被称之为异构枚举。
当然,异构枚举也被认为是很“鸡肋”的类型
// 鸡肋 enum Day { SUNDAY = 'SUNDAY', MONDAY = 2, }
枚举成员的值既可以是数字、字符串这样的常量,也可以是通过表达式所计算出来的值。这就涉及枚举里成员的一个分类,即常量成员和计算成员
当涉及的枚举成员的值都是字符串、数字字面量和未指定初始值从 0 递增数字常量,都被称作常量成员。
另外,在转译时,通过被计算的常量枚举表达式定义值的成员,也被称作常量成员,比如如下几种情况:
引用来自预先定义的常量成员,比如来自当前枚举或其他枚举;
圆括弧 () 包裹的常量枚举表达式;
在常量枚举表达式上应用的一元操作符 +、 -、~ ;
操作常量枚举表达式的二元操作符 +、-、*、/、%、<<、>>、>>>、&、|、^。除以上这些情况之外,其他都被认为是计算(值)成员。
enum FileAccess { // 常量成员 None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // 计算成员 G = "123".length, // 实际上它们也并没有太大的用处,只是告诉我们通过这些途径可以定义枚举成员的值。 // 因此,我们只需记住缺省值(从 0 递增)、数字字面量、字符串字面量肯定是常量成员就够了。
对于不需要计算(值)的常量类型成员,即缺省值(从 0 递增)、数字字面量、字符串字面量这三种情况(这就是为什么我们只需记住这三种情况),被称之为字面量枚举成员。
枚举成员的类型是枚举类型的子类型。
枚举成员和枚举类型之间的关系分两种情况:
如果枚举的成员同时包含字面量和非字面量枚举值,枚举成员的类型就是枚举本身(枚举类型本身也是本身的子类型);
如果枚举成员全部是字面量枚举值,则所有枚举成员既是值又是类型
enum Day { SUNDAY, MONDAY, enum MyDay { SUNDAY, MONDAY = Day.MONDAY const mondayIsDay: Day.MONDAY = Day.MONDAY; // ok: 字面量枚举成员既是值,也是类型 const mondayIsSunday = MyDay.SUNDAY; // ok: 类型是 MyDay,MyDay.SUNDAY 仅仅是值 const mondayIsMyDay2: MyDay.MONDAY = MyDay.MONDAY; // ts(2535),MyDay 包含非字面量值成员,所以 MyDay.MONDAY 不能作为类型
如果枚举仅有一个成员且是字面量成员,那么这个成员的类型等于枚举类型
enum Day { MONDAY export const mondayIsDay: Day = Day.MONDAY; // ok export const mondayIsDay1: Day.MONDAY = mondayIsDay as Day; // ok
不同成员的类型就是不同的字面量类型.
纯字面量成员枚举类型也具有字面量类型的特性,也就等价于枚举的类型将变成各个成员类型组成的联合(枚举)类型。
联合类型使得 TypeScript 可以更清楚地枚举集合里的确切值,从而检测出一些永远不会成立的条件判断(俗称 Dead Code)
enum Day { SUNDAY, MONDAY, const work = (x: Day) => { if (x !== Day.SUNDAY || x !== Day.MONDAY) { // ts(2367) // 在上边示例中,TypeScript 确定 x 的值要么是 Day.SUNDAY,要么是 Day.MONDAY。 // 因为 Day 是纯字面量枚举类型,可以等价地看作联合类型 Day.SUNDAY | Day.MONDAY, // 所以我们判断出第 7 行的条件语句恒为真,于是提示了一个 ts(2367) 错误
如果枚举包含需要计算(值)的成员情况就不一样了。如下示例中,TypeScript 不能区分枚举 Day 中的每个成员。因为每个成员类型都是 Day,所以无法判断出第 7 行的条件语句恒为真,也就不会提示一个 ts(2367) 错误。
enum Day { SUNDAY = +'1', MONDAY = 'aa'.length, const work = (x: Day) => { if (x !== Day.SUNDAY || x !== Day.MONDAY) { // ok }
字面量类型所具有的类型推断、类型缩小的特性,也同样适用于字面量枚举类型,如下代码所示:
enum Day { SUNDAY, MONDAY, let SUNDAY = Day.SUNDAY; // 类型是 Day const SUNDAY2 = Day.SUNDAY; // 类型 Day.SUNDAY const work = (x: Day) => { if (x === Day.SUNDAY) { x; // 类型缩小为 Day.SUNDAY }
可以通过添加 const 修饰符定义常量枚举,常量枚举定义转译为 JavaScript 之后会被移除,并在使用常量枚举成员的地方被替换为相应的内联值,因此常量枚举的成员都必须是常量成员(字面量 + 转译阶段可计算值的表达式)
const enum Day { SUNDAY, MONDAY const work = (d: Day) => { switch (d) { case Day.SUNDAY: return 'take a rest'; case Day.MONDAY: return 'work hard'; }
这里我们定义了常量枚举 Day,它的成员都是值自递增的常量成员,并且在 work 函数的 switch 分支里引用了 Day。
转译为成 JavaScript 后,Day 枚举的定义就被移除了,work 函数中对 Day 的引用也变成了常量值的引用
var work = function (d) { switch (d) { case 0 /* SUNDAY */: return 'take a rest'; case 1 /* MONDAY */: return 'work hard'; };
在 TypeScript 中,我们可以通过 declare 描述一个在其他地方已经定义过的变量.
declare let $: any; $('#id').addClass('show'); // ok
也可以使用 declare 描述一个在其他地方已经定义过的枚举类型,通过这种方式定义出来的枚举类型,被称之为外部枚举.
declare enum Day { SUNDAY, MONDAY, // 这里我们认定在其他地方已经定义了一个 Day 这种结构的枚举,且 work 函数中使用了它。 const work = (x: Day) => { if (x === Day.SUNDAY) { x; // 类型是 Day // 转译为 JavaScript 之后,外部枚举的定义也会像常量枚举一样被抹除,但是对枚举成员的引用会被保留 var work = function (x) { if (x === Day.SUNDAY) { };
外部枚举和常规枚举的差异在于以下几点:
在外部枚举中,如果没有指定初始值的成员都被当作计算(值)成员,这跟常规枚举恰好相反;
即便外部枚举只包含字面量成员,这些成员的类型也不会是字面量成员类型,自然完全不具备字面量类型的各种特性。
我们可以一起使用 declare 和 const 定义外部常量枚举,使得它转译为 JavaScript 之后仍像常量枚举一样。在抹除枚举定义的同时,我们可以使用内联枚举值替换对枚举成员的引用。
外部枚举的作用在于为两个不同枚举(实际上是指向了同一个枚举类型)的成员进行兼容、比较、被复用提供了一种途径,这在一定程度上提升了枚举的可用性,让其显得不那么“鸡肋”。
核心的几个知识点和建议:
使用常量枚举管理相关的常量,能提高代码的可读性和易维护性;
不要使用其他任何类型替换所使用的枚举成员;
指的是类型参数化,即将原来某种具体的类型进行参数化
和定义函数参数一样,我们可以给泛型定义若干个类型参数,并在调用时给泛型传入明确的类型参数。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
约束函数参数的类型,我们可以给函数定义若干个被调用时才会传入明确类型的参数。
首先,把参数 param 的类型定义为一个(类型层面的)参数、变量,而不是一个明确的类型,等到函数调用时再传入明确的类型。
比如我们可以通过尖括号 <> 语法给函数定义一个泛型参数 P,并指定 param 参数的类型为 P ,
function reflect<P>(param: P) { return param; }
尖括号中的 P 表示泛型参数的定义,param 后的 P 表示参数的类型是泛型 P(即类型受 P 约束)。
也可以使用泛型显式地注解返回值的类型,虽然没有这个必要(因为返回值的类型可以基于上下文推断出来)
可以通过尖括号 <> 语法给泛型参数 P 显式地传入一个明确的类型。
function reflect<P>(param: P):P { return param; }
注意:函数的泛型入参必须和参数/参数成员建立有效的约束关系才有实际意义。
泛型不仅可以约束函数整个参数的类型,还可以约束参数属性、成员的类型,比如参数的类型可以是数组、对象
在类的定义中,我们还可以使用泛型用来约束构造函数、属性、方法的类型,
class Memory<S> { store: S; constructor(store: S) { this.store = store; set(store: S) { this.store = store; get() { return this.store; const numMemory = new Memory<number>(1); // <number> 可缺省 const getNumMemory = numMemory.get(); // 类型是 number numMemory.set(2); // 只能写入 number 类型 const strMemory = new Memory(''); // 缺省 <string> const getStrMemory = strMemory.get(); // 类型是 string strMemory.set('string'); // 只能写入 string 类型
类型本身就可以被定义为拥有不明确的类型参数的泛型,并且可以接收明确类型作为入参,从而衍生出更具体的类型
将类型入参的定义移动到类型别名或接口名称后,此时定义的一个接收具体类型入参后返回一个新类型的类型就是泛型类型。
type ReflectFuncton = <P>(param: P) => P; interface IReflectFuncton { <P>(param: P): P const reflectFn2: ReflectFuncton = reflect; const reflectFn3: IReflectFuncton = reflect;
在泛型定义中,我们甚至可以使用一些类型操作符进行运算表达,使得泛型可以根据入参的类型衍生出各异的类型
// 定义了一个泛型,如果入参是 number | string 就会生成一个数组类型,否则就生成入参类型。而且,我们还使用了与 JavaScript 三元表达式完全一致的语法来表达类型运算的逻辑关系 type StringOrNumberArray<E> = E extends string | number ? E[] : E; type StringArray = StringOrNumberArray<string>; // 类型是 string[] type NumberArray = StringOrNumberArray<number>; // 类型是 number[] type NeverGot = StringOrNumberArray<boolean>; // 类型是 boolean
前面提到了泛型就像是类型的函数,它可以抽象、封装并接收(类型)入参,而泛型的入参也拥有类似函数入参的特性。因此,我们可以把泛型入参限定在一个相对更明确的集合内,以便对入参进行约束。
我们希望把接收参数的类型限定在几种原始类型的集合中,此时就可以使用“泛型入参名 extends 类型”语法达到这个目的
function reflectSpecified<P extends number | string | boolean>(param: P):P { return param; reflectSpecified('string'); // ok reflectSpecified(1); // ok reflectSpecified(true); // ok reflectSpecified(null); // ts(2345) 'null' 不能赋予类型 'number | string | boolean'
也可以把接口泛型入参约束在特定的范围内
interface ReduxModelSpecified<State extends { id: number; name: string }> { state: State type ComputedReduxModel1 = ReduxModelSpecified<{ id: number; name: string; }>; // ok type ComputedReduxModel2 = ReduxModelSpecified<{ id: number; name: string; age: number; }>; // ok type ComputedReduxModel3 = ReduxModelSpecified<{ id: string; name: number; }>; // ts(2344) type ComputedReduxModel4 = ReduxModelSpecified<{ id: number;}>; // ts(2344)
还可以在多个不同的泛型入参之间设置约束关系
// 在设置对象属性值的函数类型时,它拥有 3 个泛型入参:第 1 个是对象,第 2 个是第 1 个入参属性名集合的子集,第 3 个是指定属性类型的子类型(这里使用了ts keyof 操作符)。 interface ObjSetter { <O extends {}, K extends keyof O, V extends O[K]>(obj: O, key: K, value: V): V; const setValueOfObj: ObjSetter = (obj, key, value) => (obj[key] = value); setValueOfObj({ id: 1, name: 'name' }, 'id', 2); // ok setValueOfObj({ id: 1, name: 'name' }, 'name', 'new name'); // ok setValueOfObj({ id: 1, name: 'name' }, 'age', 2); // ts(2345) setValueOfObj({ id: 1, name: 'name' }, 'id', '2'); // ts(2345)
泛型入参与函数入参还有一个相似的地方在于,它也可以给泛型入参指定默认值(默认类型),且语法和指定函数默认参数完全一致,
interface ReduxModelSpecified2<State = { id: number; name: string }> { state: State type ComputedReduxModel5 = ReduxModelSpecified2; // ok type ComputedReduxModel6 = ReduxModelSpecified2<{ id: number; name: string; }>; // ok type ComputedReduxModel7 = ReduxModelSpecified; // ts(2314) 缺少一个类型参数
泛型入参的约束与默认值还可以组合使用,
// 限定了泛型 ReduxModelMixed 入参 State 必须是 {} 类型的子类型,同时也指定了入参缺省时的默认类型是接口类型 { id: number; name: string; } interface ReduxModelMixed<State extends {} = { id: number; name: string }> { state: State }
本文链接: http://www.sunqiaoyin.top/post/71.html 版权声明:本文为本站原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
没有更早的文章了...
什么是JSON?JSON(javascript object notation)全称是javascript对象表示法,它是一 种数据交换的文本格式,而不是一种编程语言,用于读取结构化数据,2001年...
原文作者:CSDN博主「Junli_1413」 原文链接:https://blog.csdn.net/qq_20901397/article/details/79982679主干流程:一、从浏览器接...
1、使用form表单进行上传文件需要为form添加enctype="multipart/form-data" 属性,除此之外还需要将表单的提交方法改成post,如下 method="post", in...
1、web安全攻击web应用的概念web应用是由动态脚本、编译过的代码等组合而成它通常架设在web服务器上,用户在web浏览器上发送请求这些请求使用http协议,由web应用和企业后台的数据库及其他动...
原文作者:拉勾教育原文链接:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=88#/detail/pc?id=2272Parcel 是一款...
很多时候都需要把相关内容生成图片,方便信息保持前端有个插件,可以根据html内容生成图片安装或导入html2canvas插件 http://html2canvas.hertzen.com/ 使用步骤:...