1.好用吗?
本质
- 是一个添加了类型注释的JavaScript
- 不会破坏JavaScript原有的知识体系
更加可靠
- TypeScript的静态类型检测, 能在开发阶段发现一些低级错误并解决
面向接口编程
比如:
interface UserInfo {
name: string;
age: number;
avatar?: string;
}
function getUserInfo(props: UserInfo) {
...
}
复制代码
- 能改变思维方式, 养成一个好的编码习惯
主流
- React、Vue 3、Angular、Deno、Nest.js 等,要么选用 TypeScript 编写源码,要么为 TypeScript 提供了完美的支持。
2. 基本语法
:
用来分割变量和类型的分隔符
let num: number = 6
复制代码
静态类型检测
在编译时期,静态类型的编程语言即可准确地发现类型错误,这就是静态类型检测的优势。
数组类型(Array)
使用 []
的形式定义数组类型
/** 子元素是数字类型的数组 */
let array1: number[] = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let array2: string[] = ['x', 'y', 'z'];
复制代码
使用 Array 泛型定义数组类型
/** 子元素是数字类型的数组 */
let array1: Array<number> = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let array2: Array<string> = ['x', 'y', 'z'];
复制代码
元组类型(Tuple)
元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。
any
any 指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式
unknown
unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量。
可以用来接收不同条件下类型各异的返回值的临时变量
let result: unknown;
if (x) {
result = x();
} else if (y) {
result = y();
}
复制代码
与 any 不同的是,unknown 在类型上更安全。比如我们可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any
let result: unknown;
let num: number = result; // 提示 ts(2322)
let anything: any = result; // 不会提示错误
复制代码
使用 unknown 后,TypeScript 会对它做类型检测。但是,如果不缩小类型(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、null
void 类型,它仅适用于表示没有返回值的函数。即如果该函数没有返回值,那它的类型就是 void。
never
never 表示永远不会发生值的类型
object
object 类型表示非原始类型的类型,即非 number、string、boolean、bigint、symbol、null、undefined 的类型
类型断言(Type Assertion)
类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。
使用 as 语法做类型断言
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 语法。
使用“字面量值 + as const”语法结构进行常量断言
/** str 类型是 '"str"' */
let str = 'str' as const;
/** readOnlyArr 类型是 'readonly [0, 1]' */
const readOnlyArr = [0, 1] as const;
复制代码
特殊非空断言,即在值(变量、属性)的后边添加 ‘!’ 断言操作符,它可以用来排除值为 null、undefined 的情况
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
// 对于非空断言来说,我们同样应该把它视作和 any 一样危险的选择。
复制代码
3.字面量类型、类型推断、类型拓宽和类型缩小
类型推断
{
let x1 = 42; // 推断出 x1 的类型是 number
let x2: number = x1; // ok
}
复制代码
上下文推断
// 通过变量所在的上下文环境推断变量的类型
{
type Adder = (a: number, b: number) => number;
const add: Adder = (a, b) => {
return a + b;
}
const x1 = add(1, 1); // 推断出 x1 类型是 number
const x2 = add(1, '1'); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number
}
复制代码
字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型
{
let specifiedStr: 'this is string' = 'this is string';
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;
}
复制代码
字面量类型是集合类型的子类型,它是集合类型的一种更具体的表达。比如 ‘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
}
复制代码
字符串字面量类型
使用一个字符串字面量类型作为变量的类型
let hello: 'hello' = 'hello';
hello = 'haha'; // ts(2322) Type '"haha"' is not assignable to type '"hello"'
复制代码
应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合
type Direction = 'up' | 'down';
function move(dir: Direction) {
// ...
}
move('up'); // ok
move('left'); // ts(2345) Argument of type '"left"' is not assignable to parameter of type 'Direction'
复制代码
数字字面量类型及布尔字面量类型
// 使用字面量组合的联合类型将函数的参数限定为更具体的类型
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
}
复制代码
Literal Widening(字面量类型的拓宽)
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
{
let str = 'this is string'; // 类型是 string
const specifiedStr = 'this is string'; // 类型是 'this is string'
}
复制代码
Type Widening(类型拓宽)
对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:
{
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
/** -----分界线------- */
const z = null; // 类型是 null
}
复制代码
Type Narrowing(类型缩小)
通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合
可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型
{
let func = (anything: any) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else if (typeof anything === 'number') {
return anything; // 类型是 number
}
return null;
};
}
复制代码
4.函数类型
返回值类型
函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型
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,
strs
} // 返回 { nums: number[]; strs: string[] } 的类型
}
复制代码
函数返回值的类型推断结合泛型, 可以实现特别复杂的类型计算,比如 Redux Model 中 State、Reducer、Effect 类型的关联。
Generator 函数的返回值
Generator 函数返回的是一个 Iterator 迭代器对象,我们可以使用 Generator 的同名接口泛型或者 Iterator 的同名接口泛型表示返回值的类型(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 类型
}
复制代码
可选参数和默认参数
function log(x?: string) {
return x;
}
log(); // => undefined
log('hello world'); // => hello world
复制代码
在类型标注的:前添加?表示 log 函数的参数 x 就是可缺省的。
函数的默认参数类型必须是参数类型的子类型
function log3(x: number | string = 'hello') {
console.log(x);
}
复制代码
剩余参数
function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
复制代码
this
在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,比如最简单的作为对象的方法的 this 指向
function say(this: Window, name: string) {
console.log(this.name);
}
window.say = say;
window.say('hi');
const obj = {
say
};
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'.
复制代码
需要注意的是,如果我们直接调用 say(),this 实际上应该指向全局变量 window,但是因为 TypeScript 无法确定 say 函数被谁调用,所以将 this 的指向默认为 void,也就提示了一个 ts(2684) 错误。
定义对象的函数属性时,只要实际调用中 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) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'
复制代码
注意:显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉,这算是 TypeScript 为数不多的特有语法。
函数重载
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
复制代码
类型谓词(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
}
}
复制代码
5.类类型
类
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog('Q');
dog.bark(); // => 'Woof! Woof!'
复制代码
继承
使用 extends 关键字就能很方便地定义类继承的抽象模式
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
复制代码
公共、私有与受保护的修饰符
在 TypeScript 中就支持 3 种访问修饰符,分别是 public、private、protected。
-
public 修饰的是在任何地方可见、公有的属性或方法;
-
private 修饰的是仅在同一类中可见、私有的属性或方法;
-
protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。
只读修饰符
使用 readonly 只读修饰符声明类的属性
class Son {
public readonly firstName: string;
constructor(firstName: string) {
this.firstName = firstName;
}
}
const son = new Son('Tony');
son.firstName = 'Jack'; // ts(2540) Cannot assign to 'firstName' because it is a read-only property.
复制代码
存取器
可以通过getter、setter截取对类成员的读写访问。
静态属性
可以给类定义静态属性和方法。
这些属性存在于类这个特殊的对象上,而不是类的实例上,所以我们可以直接通过类访问静态属性,
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 的指向默认是类的实例。
抽象类
一种不能被实例化仅能被子类继承的特殊类。
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
复制代码
类的类型
类的类型和函数类似,即在声明类的时候,其实也同时声明了一个特殊的类型(确切地讲是一个接口类型),这个类型的名字就是类名,表示类实例的类型;在定义类的时候,我们声明的除构造函数外所有属性、方法的类型就是这个特殊类型的成员。
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
复制代码
6.接口类型与类型别名
Interface 接口类型
/ ** 关键字 接口名称 */
interface ProgramLanguage {
/** 语言名称 */
name: string;
/** 使用年限 */
age: () => number;
}
复制代码
可缺省属性
/** 关键字 接口名称 */
interface OptionalProgramLanguage {
/** 语言名称 */
name: string;
/** 使用年限 */
age?: () => number;
}
let OptionalTypeScript: OptionalProgramLanguage = {
name: 'TypeScript'
}; // ok
复制代码
只读属性
interface ReadOnlyProgramLanguage {
/** 语言名称 */
readonly name: string;
/** 使用年限 */
readonly age: (() => number) | undefined;
}
let ReadOnlyTypeScript: ReadOnlyProgramLanguage = {
name: 'TypeScript',
age: undefined
}
/** ts(2540)错误,name 只读 */
ReadOnlyTypeScript.name = 'JavaScript';
复制代码
定义函数类型
interface StudyLanguage {
(language: ProgramLanguage): void
}
/** 单独的函数实践 */
let StudyInterface: StudyLanguage
= language => console.log(`${language.name} ${language.age()}`);
复制代码
索引签名
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’ 索引对象时,这两者等价。
{
interface LanguageRankInterface {
readonly [rank: number]: string;
}
interface LanguageYearInterface {
readonly [name: string]: number;
}
}
// LanguageRankInterface 和 LanguageYearInterface 任意的数字或者字符串类型的属性都是只读的。
复制代码
继承与实现
可以使用如下所示的 extends 关键字实现接口的继承。
{
interface DynamicLanguage extends ProgramLanguage {
rank: number; // 定义新属性
}
interface TypeSafeLanguage extends ProgramLanguage {
typeChecker: string; // 定义新的属性
}
/** 继承多个 */
interface TypeScriptLanguage extends DynamicLanguage, TypeSafeLanguage {
name: 'TypeScript'; // 用原属性类型的兼容的类型(比如子集)重新定义属性
}
}
复制代码
注意:我们仅能使用兼容的类型覆盖继承的属性
{
/** ts(6196) 错误的继承,name 属性不兼容 */
interface WrongTypeLanguage extends ProgramLanguage {
name: number;
}
}
// 因为 ProgramLanguage 的 name 属性是 string 类型,WrongTypeLanguage 的 name 属性是 number,二者不兼容,所以不能继承,也会提示一个 ts(6196) 错误。
复制代码
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'];
}
复制代码
Interface 与 Type 的区别
- 重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展
{
interface Language {
id: number;
}
interface Language {
name: string;
}
let lang: Language = {
id: 1, // ok
name: 'name' // ok
}
}
复制代码
- 重复定义类型别名,如下代码所示,则会提示一个 ts(2300) 错误
{
/** ts(2300) 重复的标志 */
type Language = {
id: number;
}
/** ts(2300) 重复的标志 */
type Language = {
name: string;
}
let lang: Language = {
id: 1,
name: 'name'
}
}
复制代码
7. 高级类型:联合类型和交叉类型
联合类型
联合类型(Unions)用来表示变量、参数的类型不是单一原子类型,而可能是多种不同的类型的组合。
通过“|”操作符分隔类型的语法来表示联合类型
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' 类型
复制代码
可以组合任意个、任意类型来构造更满足我们诉求的类型
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
}
复制代码
交叉类型
把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性
合并接口类型
联合类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型
type IntersectionType = { id: number; name: string; }
& { age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
复制代码
如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 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 属性的类型就是两者中的子类型。
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
};
复制代码
合并联合类型
可以合并联合类型为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员。这里,我们也可以将合并联合类型理解为求交集。
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)
复制代码
联合、交叉组合
联合操作符 | 的优先级低于交叉操作符 &,同样,我们可以通过使用小括弧 () 来调整操作符的优先级。
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 了
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
复制代码
缩减,却极大地削弱了 IDE 自动提示的能力,
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // 类型缩减成 string
// 在上述代码中,我们希望 IDE 能自动提示显示注解的字符串字面量,但是因为类型被缩减成 string,所有的字符串字面量 black、red 等都无法自动提示出来了。
// TypeScript 官方其实还提供了一个黑魔法,它可以让类型缩减被控制。如下代码所示,我们只需要给父类型添加“& {}”即可
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // 字面类型都被保留
复制代码
8.枚举类型
枚举类型
enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
复制代码
7 种常见的枚举类型:
数字类型、字符串类型、异构类型、常量成员和计算(值)成员、枚举成员类型和联合枚举、常量枚举、外部枚举。
- 数字枚举: 在仅仅指定常量命名的情况下,我们定义的就是一个默认从 0 开始递增的数字集合,称之为数字枚举。如果我们希望枚举值从其他值开始递增,则可以通过“常量命名 = 数值” 的格式显示指定枚举成员的初始值
enum Day {
SUNDAY = 1,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
复制代码
给 SUNDAY 指定任意类型(比如整数、负数、小数等)、任意起始的数字,其后未显示指定值的成员会递增加 1
- 字符串枚举: 将定义值是字符串字面量的枚举称之为字符串枚举,字符串枚举转译为 JavaScript 之后也将保持这些值
enum Day {
SUNDAY = 'SUNDAY',
MONDAY = 'MONDAY',
...
}
复制代码
-
异构枚举(Heterogeneous enums): TypeScript 支持枚举类型同时拥有数字和字符类型的成员,这样的枚举被称之为异构枚举。
-
常量成员和计算(值)成员: 在前边示例中,涉及的枚举成员的值都是字符串、数字字面量和未指定初始值从 0 递增数字常量,都被称作常量成员。在转译时,通过被计算的常量枚举表达式定义值的成员,也被称作常量成员,比如如下几种情况:
-
引用来自预先定义的常量成员,比如来自当前枚举或其他枚举;
-
圆括弧 () 包裹的常量枚举表达式;
-
在常量枚举表达式上应用的一元操作符 +、 -、~ ;
-
操作常量枚举表达式的二元操作符 +、-、*、/、%、<<、>>、>>>、&、|、^。
除以上这些情况之外,其他都被认为是计算(值)成员。
enum FileAccess { // 常量成员 None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // 计算成员 G = "123".length, } 复制代码
-
-
枚举成员类型和联合枚举
枚举成员和枚举类型之间的关系分两种情况: 如果枚举的成员同时包含字面量和非字面量枚举值,枚举成员的类型就是枚举本身(枚举类型本身也是本身的子类型);如果枚举成员全部是字面量枚举值,则所有枚举成员既是值又是类型
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 不能作为类型 复制代码
-
常量枚举(const enums)
const enum Day { SUNDAY, MONDAY } const work = (d: Day) => { switch (d) { case Day.SUNDAY: return 'take a rest'; case Day.MONDAY: return 'work hard'; } } 复制代码
-
外部枚举(Ambient enums): 通过 declare 描述一个在其他地方已经定义过的变量
declare let $: any; $('#id').addClass('show'); // ok 复制代码
-
外部枚举和常规枚举的差异在于以下几点:
-
在外部枚举中,如果没有指定初始值的成员都被当作计算(值)成员,这跟常规枚举恰好相反;
-
即便外部枚举只包含字面量成员,这些成员的类型也不会是字面量成员类型,自然完全不具备字面量类型的各种特性。
-
9.泛型
泛型指的是类型参数化,即将原来某种具体的类型进行参数化。和定义函数参数一样,我们可以给泛型定义若干个类型参数,并在调用时给泛型传入明确的类型参数。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
泛型类型参数
function reflect<P>(param: P) {
return param;
}
复制代码
泛型不仅可以约束函数整个参数的类型,还可以约束参数属性、成员的类型,比如参数的类型可以是数组、对象
function reflectArray<P>(param: P[]) {
return param;
}
const reflectArr = reflectArray([1, '1']); // reflectArr 是 (string | number)[]
复制代码
泛型类
在类的定义中,我们还可以使用泛型用来约束构造函数、属性、方法的类型
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 类型
复制代码
泛型类型
const reflectFn: <P>(param: P) => P = reflect; // ok
复制代码
type ReflectFuncton = <P>(param: P) => P;
interface IReflectFuncton {
<P>(param: P): P
}
const reflectFn2: ReflectFuncton = reflect;
const reflectFn3: IReflectFuncton = reflect;
复制代码
注意:枚举类型不支持泛型。
泛型约束
把泛型入参限定在一个相对更明确的集合内,以便对入参进行约束。
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'
复制代码