typescript学习笔记——高级类型

2019 / 06 / 08

typescript的文档非常庞大,但是其实使用一两个礼拜之后再去看文档会发现其实内容并没有那么多,很多说明其实是非常显而易见的。typescript把这一篇称作高级类型,但我觉得这些类型并没有什么高级可言,可能是我对高级这个词汇抱着一种等级制度的误解,就好像编程中高级语言其实并不会比低级语言强大,比如javascript和python是高级语言,c是低级语言。

ts中的高级类型更像是入门了基本的类型之后的进阶知识,引导你更加灵活的使用typescript。

高级类型

高级类型主要包括两种交叉类型和联合类型,这两种类型也没什么高级的,只是他们是通过普通的类型通过关系运算符复合而来的。

交叉类型

交叉类型更加类似于且,就是说一个值的类型需要同时满足多个类型的约束。

interface Worker {
profission: string
skill: string
}

interface Chinese {
firstName: string
lastName: string
}

type ChineseWorker = Worker & Chinese

function applyWork(people: Chinese, worker: Worker): ChineseWorker {
return Object.assign(people, worker)
}

联合类型

联合类型更加类似于或,就是说一个值的类型只需要满足多个类型中的一个就行了。

interface Boy {
sex: 'boy'
girlFriend?: Girl
}

interface Girl {
sex: 'girl'
boyFriend?: Boy
}

const people: Boy | Girl = {
sex: 'boy',
girlFriend: null
}

理解这两种类型还是需要花点时间的,因为这个且或关系和平时所用到差异还是蛮大的。

字面量类型

在定义boy的时候我们写了一个sex: 'boy',这里就是定义了一个字面量作为了类型值,也就是说这个类型他只会为boy,他不是一个字符串也不是个enum,他就是一个固定的值,如果要输入一个boy类型的值,那么必须写上sex = 'boy'。甚至字符串的'boy'并不是这个类型'boy'。

type Boy = {
sex: 'boy'
girlFriend?: string
}

字面量可以是字符串也可以是true和false或者数值,可以是任何基本类型的字面量值,他看起来可能没什么用,但是用来区分类型非常管用。

类型保护

高级类型对类型进行了组合,但是这也带来了一个问题,输入的不确定性。比如

function increase(x: string | number) {
return x + 1 // Error: Operator '+' cannot be applied to types 'string | number' and '1'
}

这个时候就需要用到类型保护来解决这个问题

typeof

最简单最常用的方式,通过typeof来判断类型,然后作不同的处理

function increase(x: string | number) {
typeof x === 'string' ? parseInt(x) + 1 : x + 1
}

instanceof

如果是一个对象,那么可以通过instanceof来判断

class Boy {
sex: 'boy'
girlFriend?: Girl
}

class Girl {
sex: 'girl'
boyFriend?: Boy
}

function assignFriend(people: Boy | Girl) {
if (people instanceof Boy) {
people.girlFriend = new Girl()
}
if (people instanceof Girl) {
people.boyFriend = new Boy()
}
}

可辨识联合

可辨识的联合也就是利用字面量类型的特点,通过字面量进行类型保护,同样是上面的demo,如果通过instance区分的话,看起来可能比较复杂,而通过字面量区分的话就会非常的清晰明了。这也是字面量类型非常大的用处,可以很方便的进行类型保护判断。

const getFriend = (people: Boy | Girl) => {
if (people.sex === 'boy') {
return people.girlFriend
}
if (people.sex === 'girl') {
return people.boyFriend
}
}

type

type这个语法在官方翻译中是“类型别名”,他和interface没有什么关系,只是帮助你给你的类型起个别名。他的使用方法更像是linux中的alias。

type Name = string

但是我觉得这个还是理解为定义类型比较好,就像定义某个变量或者函数一样。我们可以随意的定义任何的类型,比如各种各样的函数定义。

type toString = () => string
type genericArrayify<T> = (T) => [T]
type numberArrayify = genericArrayify<number>

当然我们也可以定义对象类型,就像接口interface那样

type Boy = {
sex: 'boy'
girlFriend?: Girl
}

type Girl = {
sex: 'girl'
girlFriend?: Boy
}

类型甚至可以被实现,因为这个方式定义的type就是一种接口,这里隐式的实现了一个接口,并且复制给了Boy这个type,所以Boy本身是一个接口类型。

type Boy = {
sex: 'boy'
girlFriend?: string
}

class LittleBoy implements Boy {
// ...
}

所以type和interface的区别是什么呢

找了很多的博客,也看了官方的文档,他们会告诉你type基本上可以实现任何interface可以实现的功能,唯独继承和实现,比如type去extends在语法上是错误的,比如interface会发生合并。

type LittleBoy extends Boy { // Error 
interface LittleBoy extends Boy { // Right

当然这些只是表象,如果你使用一个礼拜的typescript之后你会发现其实并没有那么复杂,二者的关系就好像js中的let和class一样,let是用来进行赋值的,而class是用来声明对象的。

所以其实在ts的世界里,类型就是最基本的单位,interface也是一种类型,但是类型在语法上是无法面向对象的,就好像 let A extends B一样在js中是错误的。

而一一列觉let和class的区别很显然是非常不明智的,因为他们并没有多大的可比性。当然ts中type和interface的关系可能没有let和class区分的那么显而易见。因为站在使用者的角度来看interface和type都是在声明类型。

但其实这并不是一个值得纠结的问题。构建面向对象的类型世界,我们更应该使用interface,而处理灵活多变的类型组合和重定义我们应该使用type。

索引类型 keyof

刚刚说了在ts的世界里类型是最基本的单位,而索引也是一种类型。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}

let x = { a: 1, b: 2, c: 3, d: 4 }
getProperty(x, 'm') // error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'

keyof可以理解为ts中的一个运算符。通过keyof可以拿到object类型或者数组类型的键。

可以非常简单的使用

type List = []
const listProps: keyof List = 1
const listLengthProps: keyof List = 'length'

interface Person {
name: string
age: number
skill: string
}

const personProps: keyof Person = 'name'

映射类型 in

当我们有一个需求,想要复制一个一摸一样的Person类,但是属性是只读的,或者所有属性都是可选的。比如下面这样,但是维护三分代码很显然比较麻烦。

interface Person {
name: string
sex: string
age: number
}

interface ReadonlyPerson {
readonly name: string
readonly sex: string
readonly age: string
}

interface OptionalPerson {
name?: string
sex?: string
age?: string
}

in和keyof

这个时候就用到了映射,映射这个词其实就是js的map

interface Person {
name: string
sex: string
age: number
}

type ReadonlyWrapper<T> = { readonly [K in keyof T]: T[K] }
type ReadonlyPerson = ReadonlyWrapper<Person>

const person: ReadonlyPerson = {
name: 'xiaohan',
sex: 'boy',
age: 18
}

person.name = 'xiao wang' // Error: Cannot assign to 'name' because it is a read-only property

映射是针对一个列表做的处理,所以需要映射某个接口的时候需要先使用keyof,keyof和in是经常一起出现的好兄弟。

in和联合类型

in和联合类型也能很好的一起使用

type Keys = 'sex' | 'name' | 'address' | 'city' | 'phone'
type Person = { [K in Keys]: string | null | undefined }

注意,in和keyof操作符只能用在type中,不能用在interface中,这也是type和interface的区别,但我还是不建议你记住这个区别,就像let后边接表达式很正常,而class后面的运算符只能是extends或者implements一样,let和type一样是赋值语句的声明,而interface和class是用来表示对象的继承关系。

标准库工具

Readonly是一个非常好用的工具,非常人性化的ts官方其实在类型标准库里已经事先定义好了许多工具,比如Readonly和Partial,以及Pick和Record

他们都非常的好用

type Readonly<T> = {
readonly [P in keyof T]: T[P];
}

type Partial<T> = {
[P in keyof T]?: T[P];
}

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
type Record<K extends string, T> = {
[P in K]: T;
}

除了这些ts还有

Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
Extract<T, U> -- 提取T中可以赋值给U的类型。
NonNullable<T> -- 从T中剔除null和undefined。
ReturnType<T> -- 获取函数返回值类型。
InstanceType<T> -- 获取构造函数类型的实例类型。

嗨,请先 登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟