面向Type编程 -- Typescript类型和类型操作(一)

197 阅读8分钟

简述

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等