简述
Typescript发展至今,已经成为大型项目标配。其提供的静态类型系统,大大增强了代码的可读性以及可维护性。尽管如此,在很多开发者意识里,依然认为Typescript仅仅是javascript的超集,或者有类型限制的javascript。这样描述本身并没有错,但未能触及Typescript的核心。Typescript的核心是类型(Type)以及类型操作。只有认识和熟练掌握了类型操作,能够像操作语言一样操作类型,才算真正窥探Typescript的内在意义。
从认识类型操作符开始
面向Type编程是一个伪概念,软件工程中不存在这种概念。为了提高对Type以及Type oprator的关注度,我刻意创造出来的一个不规范名词。引导大家从了解和掌握类型操作角度,去重新认识Typescript编程。
Typescript中有很多类型操作符,例如用来声明类型和接口的type和interface;还有关键字typeof、keyof、in、extends、infer;以及()、[]、{}、=、<>、:、;、?、&、|等界限符。下面我们一起来初步认识一下这些熟悉而又陌生的类型操作符,这里我们主要讨论关键字操作符。
一、 typeof操作符
1. js表达式中typeof用来返回一个变量的基本数据类型
,如'string'、'number'、'function'、'object'
typeof 1 // 'number'
typeof true // boolean
typeof 'hello world' // 'string'
typeof function(){} // 'function'
typeof {} // 'object'
typeof [] // 'object'
typeof null // 'object'
typeof undefined // 'undefined'
typeof new RegExp() // 'object'
typeof Symbol() // symbol
2. 在Typescript中,typeof可以获取变量的声明类型
typeof在typescript中还可以用来返回一个变量的声明类型,如果不存在,则获取该类型的推论类型。typeof在上下文中的作用决定了返回值是什么。是类型查询?还是表达式?前者是返回类型(types),而后者返回类型值(values)。例如,console.log、if判断、变量声明let,var等操作中typeof返回变量的类型的字符串名称,而type关键字限制下,typeof返回变量的类型
例如:
let n: number = 1
type TN = typeof n
/** 等同于
* TN == number
*/
let s:string = 'hello world'
type TS = typeof s
/** 等同于
* TS == string
*/
let a: Array<number> = []
type TA = typeof a
/** 等同于
* TA == number[]
*/
let sy: Symbol = Symbol()
type TSY = typeof sy
/** 等同于
* TSY == Symbol
*/
这里有一点需要说明,不能单纯的通过关键字,判断typeof在上下文中的作用。而是综合的来判断是属于表达式还是类型查询。比如,下面例子中,虽然出现了let关键字,但是很明显typeof出现在类型定声明的位置上,因此这里是类型查询,typeof s返回s2的声明类型string
let s2: string = 'hello'
let s3: typeof s = 'world'
let s4: typeof s = 123 // 错误,不能将类型“number”分配给类型“string”
3. typeof作为类型操作符后面只能跟变量
type T = typeof 's' // 错误
let s = 's'
type T = typeof s // 正确
4. typeof返回对象字面量的结构类型
如果typeof的类型操作的变量值是对象字面量,返回的类型并不是object,而是相同结构的类型字面量
let obj = {name:'zhangsan', age:28, male:true, run:()=>'run'}
type TO = typeof obj
/** 等同于
* type TO = {
* name: string;
* age: number;
* male: boolean;
* run: () => string;
* }
*/
而且,这种操作对字面量嵌套同样生效
let obj1 = {
name: 'zhangsan',
child: {
name: 'lisi',
age: 32
},
age: 28,
male: true,
run: () => 'run'
}
type TO1 = typeof obj1
/** 等同于
* type TO1 = {
* name: string;
* age: number;
* child:{
* name:string;
* age: number;
* },
* male: boolean;
* run: () => string;
* }
*/
5. 变量没有声明类型,typeof返回变量的推断类型
如果变量没有明确声明类型,typeof将返回变量的推断类型。此时,let关键字声明的变量,可以被重新分配
let s1 = 'hello'
type TS = typeof s1 // TS = string
// 可以被重新赋值
s1 = 'world'
s1 = 1 // 错误 不能将类型“number”分配给类型“string”
有时候,我们希望变量是常量,不允许被重新分配。const关键字可以解决这个问题,确保不会发生对变量进行重新分配。此时,基于类型推断,返回类型是等号右边的字面量类型。例如,下面例子中typeof返回的'hello',是字面量类型,不是字符串,这一点要明确
const s2 = 'hello'
type TS = typeof s2 // TS = 'hello'
s2 = 'world' // 错误 无法分配到 "s2" ,因为它是常数
Typescript3.4引入了一种新的字面量构造方式,const断言。在const断言作用下,即使是let声明也可以限制类型扩展,变量不能被重新分配,typeof返回变量s3字面量类型'hello'
let s3 = 'hello' as const
type TS = typeof s3 // TS = 'hello'
s3 = 'world' // 错误,不能将类型“"world"”分配给类型“"hello"”
因为,当我们使用const断言构造新的字面量表达式时,向编程语言发出以下信号:
- 表达式中的任何字面量类型都不应该被扩展
- 对象字面量的属性,将使用 readonly 修饰
- 数组字面量将变成 readonly 元组
let x = "hello" as const;
type X = typeof x; // type X = "hello"
let y = [10, 20] as const;
type Y = typeof y; // type Y = readonly [10, 20]
let z = { text: "hello" } as const;
type Z = typeof z; // let z: { readonly text: "hello"; }
如果变量明确声明了类型,推断类型不受const影响,typeof返回s4的声明类型string,而不是字面类型'hello'。但是变量依然不能被重新分配
const s4:string = 'hello'
type TS = typeof s4 // TS = string
s4 = 'world' // 错误,无法分配到 "s4" ,因为它是常数
如果变量的值是对象字面量,其推断类型不受const影响
const person = {
name: 'zhangsan',
age: 28,
male: true,
run: () => 'run'
}
type TP = typeof person
/** 等同于
type TP = {
name: string;
age: number;
male: boolean;
run: () => string;
}
*/
const arr = [1]
type TARR = typeof arr // type TARR = number[]
const fn = ()=>'val'
type TFN = typeof fn // type TFN = () => string
boolean类型是个特殊的存在,typeof作用下将被收窄
let b = true // 推断 b:boolean
type TB = typeof b // TB = true
b = false
type TB1 = typeof b // TB1 = false
let b1: boolean = false
type TB3 = typeof b1 // TB3 = false
b1 = true
type TB4 = typeof b1 // TB4 = true
const b2 = false // 推断b2:false
type TB5 = typeof b2 // TB5 = false
b2 = true // 错误 无法分配到 "b2" ,因为它是常数
const b3: boolean = true
type TB6 = typeof b3 // TB6 = true
b3 = false // 错误 无法分配到 "b3" ,因为它是常数
有兴趣的可以思考一下,下面这个例子 let T = typeof 0 type TT = typeof T TT = ?\
二、keyof操作符
TypeScript允许我们遍历某种类型的属性,并通过keyof操作符提取其属性的名称,类似Object.keys方法。keyof操作符是在TypeScript 2.1版本引入的,可以用于获取某种类型的所有键,其返回类型是联合类型
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string
1. keyof也可以用于操作类
class Person {
name: string = "Semlinker";
}
let sname: keyof Person; //等同于 let sname: 'name'
sname = 'name'
snmae = 'age' // 异常,不能将类型“"age"”分配给类型“"name"”
2. keyof操作符除了支持接口和类之外,它也支持基本数据类型
type TB = keyof boolean // 'toString'
type TN = keyof number // "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type TS = keyof string // number | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" ...
type TARR = keyof any[] // number | "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" ...
type TSY = keyof symbol // "toString" | "valueOf"
type TNULL = keyof null // never
type TVOID = keyof void // never
type TUDF = keyof undefined // never
有兴趣的可以思考一下 type TA = keyof any TA = ?\
3. keyof可以结合typeof一起使用
- keyof可以结合typeof一起使用,用于获取变量声明类型的key值的联合类型。这也给了我们一种获取联合类型的方式
let colors = {
red: 'Red',
green:'Green',
blue:'Blue'
}
type TColors = keyof typeof colors // 'red' | 'green' | 'blue'
- keyof可以获取类型的映射类型。当然同样可以结合typeof一起使用,获取变量声明类型的映射类型。映射类型使用非常普遍和实用。
let colors = {
red: 'Red',
green:'Green',
blue:'Blue'
}
type Colors = typeof colors
type ColorMap = {
[p in keyof Colors]?: Colors[p]
}
/* 等同于
* type ColorMap = {
red?: string;
green?: string;
blue?: string;
}
*/
上面实例,我们通过结合keyof和in操作符,遍历了类型Colors,创建了它的映射类型ColorMap,把相应的类型选项都变成了可选。这个实例非常典型,如果把Colors当做类型变量,结合泛型,可以创造出更具通用性的类型。可以看出,无论从声明结构,还是使用过程都跟函数表达式极其相似。请记住这就是面向Type编程的典型实例,我们将在下一章详细讨论这个话题。
特别说明,这里使用了in类型操作符,后面会讲到\
type Partial<T> = {
[P in keyof T]?: T[P]
}
type ColorMap1 = Partial<Colors>
/* 同样可以得到
* type ColorMap1 = {
red?: string;
green?: string;
blue?: string;
}
*/
4. keyof结合[]一起用于类型索引访问查询
interface Person {
name: string;
age: number;
beard: boolean;
}
type Vlues = Person[keyof Person]
// => Person['name' | 'age' | 'beard']
// => Person['name'] | Person['age'] | Person['beard']
// => string | number | boolean
[]操作符可以用来访问目标类型某属性的类型,类似于对象索引访问。参照下面两个实例,对比理解会更加清晰
对象索引访问
let person = {
name:'zhangsan',
age:28,
child:{
name:'zhangxiaosan',
age:5
}
}
let name = person['name'] // 'zhangsan'
let child = person['child'] // {name:'zhangxiaosan', age:5}
let name1 = person['child']['name'] // 'zhangxiaosan'
类型的索引访问
type PersonType = typeof person
/**
* type PersonType = {
name: string;
age: number;
child: {
name: string;
age: number;
};
}
*/
type TName = PersonType['name'] // string
type TChild = PersonType['child']
/** 获取到
* child: {
name: string;
age: number;
}
*/
type TChildName = PersonType['child']['name'] // string
5. keyof的用途和作用
keyof主要用于对类型成员进行操作或者限制时使用。比如,筛选出特定成员、限制取值范围、给成员增加获取去除属性等
例如,获取对象的成员函数
function getProp(obj: object, key: string) {
return obj[key];
}
let person = {
name:'zhangsan',
age:28,
male:true
}
let name = getProp(person, 'name')
显然,上面key的类型声明为string,限制范围太广泛了。更为精确的类型限制范围应该是person对象key值字面量类型的联合类型'name' | 'age' | 'male'。前面讲过结合使用keyof 和 typeof可以获取对象的key的的联合类型 ,从而实现精确限定key取值范围的目的
let T = typeof person
let K = keyof T
function getProp(obj: T, key: K) {
return obj[key];
}
这种写法依然有缺陷,obj是未知的,不一定是person,没有办法提前获取到T和K。更合理的方式应该借助泛型,实现动态传入类型,最终的实现方式:
function getProp<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
getProp<typeof person,keyof typeof person>(person,'name')
// 或者
getProp(person,'name')
下一章节,将继续介绍Typescript类型操作符:extends、in、infer等