交叉类型 交叉类型是将多个类型合并为⼀个类型。 这让我们可以把现有的多种类型叠加到⼀起成为⼀种类型, 它包含了所需的所有类型的特性。
在 JavaScript 中,混⼊是⼀种⾮常常⻅的模式,在这种模式中,你可以从两个对象中创建⼀个新对 象,新对象会拥有着两个对象所有的功能。
function mixin < T extends object , U extends object > ( first : T , second : U ) : T & U { const result = < T & U > { } ; for ( let id in first ) { ( < T > result ) [ id ] = first [ id ] ; } for ( let id in second ) { if ( ! result . hasOwnProperty ( id ) ) { ( < U > result ) [ id ] = second [ id ] ; } } return result ; } const x = mixin ( { a : 'hello' } , { b : 42 } ) ; console . log ( x . a ) ; console . log ( x . b ) ; Copy 联合类型 在 JavaScript 中,希望属性为多种类型之⼀,如字符串或者数组。 这就是联合类型所能派上⽤场的地⽅(它使⽤ | 作为标记,如 string | number)。
function formatCommandline ( command : string [ ] | string ) { let line = '' ; if ( typeof command === 'string' ) { line = command . trim ( ) ; } else { line = command . join ( ' ' ) . trim ( ) ; } } Copy 类型别名 type some = boolean | string const b : some = true const c : some = 'hello' const d : some = 123 type Tree < T > = { value : T ; left : Tree < T > ; right : Tree < T > ; } Copy type和interface的区别:
interface 只能⽤于定义对象类型,⽽ type 的声明⽅式除了对象之外还可以定义交叉、联合、原始类 型等,类型声明的⽅式适⽤范围显然更加⼴泛。
但是interface也有其特定的⽤处:
interface ⽅式可以实现接⼝的 extends 和 implements interface 可以实现接⼝合并声明 type Alias = { num : number } interface Interface { num : number ; } declare function aliased ( arg : Alias ) : Alias ; declare function interfaced ( arg : Interface ) : Interface ; Copy 接⼝创建了⼀个新的名字,可以在其它任何地⽅使⽤,类型别名并不创建新名字,⽐如,错误信息就不会使⽤别名。
可辨识联合类型 先假设⼀个场景,现在⼜两个功能,⼀个是创建⽤⼾即 create ,⼀个是删除⽤⼾即 delete . 我们先定义⼀下这个接⼝,由于创建⽤⼾不需要id,是系统随机⽣成的,⽽删除⽤⼾是必须⽤到 id 的,那么 代码如下:
interface Info { username : string } interface UserAction { id ? : number action : 'create' | 'delete' info : Info } Copy 上⾯的接⼝是不是有什么问题? 是的,当我们创建⽤⼾时是不需要 id 的,但是根据上⾯接⼝产⽣的情况,以下代码是合法的:
const action : UserAction = { action : 'create' , id : 111 , info : { username : 'xiaomuzhu' } } Copy 但是我们明明不需要 id 这个字段,因此我们得⽤另外的⽅法,这就⽤到了上⾯提到的「字⾯量类型」了:
interface Info { username : string } type UserAction = { id : number action : 'delete' info : Info } | { action : 'create' info : Info } const UserReducer = ( userAction : UserAction ) => { switch ( userAction . action ) { case 'delete' : console . log ( userAction . id ) ; break ; default : break ; } } interface Person { name : string ; age : number ; } const person = { } as Person ; person . name = 'xiaomuzhu' ; person . age = 20 ; Copy 字面量类型 字⾯量(Literal Type)主要分为 真值字⾯量类型(boolean literal types),数字字⾯量类型 (numeric literal types),枚举字⾯量类型(enum literal types),⼤整数字⾯量类型(bigInt literal types)和字符串字⾯量类型(string literal types)。
const a : 2333 = 2333 const ab : 0b10 = 2 const ao : 0o114 = 0b1001100 const ax : 0x514 = 0x514 const b : 0x1919n = 6425n const c : 'xiaomuzhu' = 'xiaomuzhu' const d : false = false const g : 'github' = 'pronhub' Copy 当字⾯量类型与联合类型结合的时候,⽤处就显现出来了,它可以模拟⼀个类似于枚举的效果:
type Direction = 'North' | 'East' | 'South' | 'West' ; function move ( distance : number , direction : Direction ) { } Copy 类型字面量 类型字⾯量(Type Literal)不同于字⾯量类型(Literal Type),它跟 JavaScript 中的对象字⾯量的语法很 相似:
type Foo = { baz : [ number , 'xiaomuzhu' ] ; toString ( ) : string ; readonly [ Symbol . iterator ] : 'github' ; 0x1 : 'foo' ; "bar" : 12n ; } ; Copy 类型断言 初学者经常会遇到的⼀类问题:
const person = { } ; person . name = 'xiaomuzhu' ; person . age = 20 ; Copy 这个时候该怎么办?由于类型推断,这个时候 person 的类型就是 {} ,根本不存在后添加的那些属 性,虽然这个写法在js中完全没问题,但是开发者知道这个 person 实际是有属性的,只是⼀开始没 有声明⽽已,但是 typescript 不知道啊,所以就需要类型断⾔了:
interface Person { name : string ; age : number ; } const person = { } as Person ; person . name = 'xiaomuzhu' ; erson . age = 20 ; Copy 双重断言 interface Person { name : string ; age : number ; } const person = 'xiaomuzhu' as Person ; const person = 'xiaomuzhu' as any as Person ; Copy 类型守卫 intanceof、in
class Person { name = 'xiaomuzhu' ; age = 20 ; } class Animal { name = 'petty' ; color = 'pink' ; } function getSometing ( arg : Person | Animal ) { if ( arg instanceof Person ) { console . log ( arg . color ) ; console . log ( arg . age ) ; } if ( arg instanceof Animal ) { console . log ( arg . color ) ; console . log ( arg . age ) ; } } function getSometing ( arg : Person | Animal ) { if ( 'age' in arg ) { console . log ( arg . color ) ; console . log ( arg . age ) ; } if ( 'color' in arg ) { console . log ( arg . age ) ; console . log ( arg . color ) ; } } Copy 类型兼容性 结构类型 TypeScript ⾥的类型兼容性是基于「结构类型」的,结构类型是⼀种只使⽤其成员来描述类型的⽅ 式,其基本规则是,如果 x 要兼容 y,那么 y ⾄少具有与 x 相同的属性。x=y 我做⼀个简单的实验,构建⼀个类 Person ,然后声明⼀个接⼝ Dog , Dog 的属性 Person 都拥有,⽽且还多了其他属性,这种情况下 Dog 兼容了 Person 。
class Person { constructor ( public weight : number , public name : string , public born : string ) { } } interface Dog { name : string weight : number } let x : Dog x = new Person ( 120 , 'cxk' , '1996-12-12' ) Copy 但反过来就不行,总结小的兼容大的
函数的兼容性 函数类型的兼容性判断,要查看 x 是否能赋值给 y,⾸先看它们的参数列表。 x 的每个参数必须能在 y ⾥找到对应类型的参数,注意的是参数的名字相同与否⽆所谓,只看它们的类 型。
let q = ( a : number ) => 0 ; let y = ( b : number , s : string ) => 0 ; y = q ; q = y ; let foo = ( x : number , y : number ) => { } ; let bar = ( x ? : number , y ? : number ) => { } ; let bas = ( ... args : number [ ] ) => { } ; let foo2 = ( x : number , y : number ) => { } ; let bar2 = ( x ? : number ) => { } ; Copy 参数多的兼容参数少的,也就是参数少的可以赋值给参数多的
类的类型兼容性 class Animal { feet : number ; constructor ( name : string , numFeet : number ) { this . feet = numFeet } } class Size { feet : number ; constructor ( meters : number ) { this . feet = meters } } let a : Animal = new Animal ( 'a' , 2 ) ; let s : Size = new Size ( 1 ) ; a = s ; s = a ; Copy 泛型的类型兼容性 泛型本⾝就是不确定的类型,它的表现根据是否被成员使⽤⽽不同.
interface Person < T > { } let x : Person < string > let y : Person < number > x = y y = x Copy 由于没有被成员使⽤泛型,所以这⾥是没问题的。
接着看如下案例:
interface Person < T > { name : T } let x : Person < string > let y : Person < number > x = y y = x Copy is关键字 function isString ( test : any ) : test is string { return typeof test === 'string' ; } function example ( foo : number | string ) { if ( isString ( foo ) ) { console . log ( 'it is a string' + foo ) ; console . log ( foo . length ) ; } else { console . log ( foo ) } } example ( 'hello world' ) ; Copy 可调⽤类型注解 interface ToString { ( ) : string new ( ) : string } declare const sometingToString : ToString ; sometingToString ( ) new sometingToString ( ) Copy 高级类型之索引类型、映射类型、条件类型 索引类型 先看⼀个场景,现在我们需要⼀个 pick函数,这个函数可以从对象上取出指定的属性,类似于 lodash.pick ⽅法。
javascript:
function pick ( o , names ) { return names . map ( n => o [ n ] ) ; } const user = { username : 'Jessica Lee' , id : 460000201904141743 , token : '460000201904141743' , avatar : 'http://dummyimage.com/200x200' , role : 'vip' } const res = pick ( user , [ 'id' ] ) console . log ( res ) Copy typescript简陋版:
interface Obj { [ key : string ] : any } function pick ( o : Obj , names : string [ ] ) { return names . map ( n => o [ n ] ) ; } Copy 高级框架版:
type key = keyof T === 'username' | 'id' ... function pick < T , K extends keyof T > ( o : T , names : K [ ] ) : T [ K ] [ ] { return names . map ( n => o [ n ] ) } const res = pick ( user , [ 'token' , 'id' , ] ) Copy 映射类型 有⼀个User接⼝,现在有⼀个需求是把User接⼝中的成员全部变成可选的,我们应该怎么做?难 道要重新⼀个个 : 前⾯加上 ? ,有没有更便捷的⽅法?
这个时候映射类型就派上⽤场了,映射类型的语法是 [K in Keys] :
K:类型变量,依次绑定到每个属性上,对应每个属性名的类型 Keys:字符串字⾯量构成的联合类型,表⽰⼀组属性名(的类型) type partial < T > = { [ K in keyof T ] ? : T [ K ] } Copy interface User3 { username : string id : number token : string avatar : string role : string } type Keyof = keyof User3 type partial < T > = { [ K in keyof T ] ? : T [ K ] } type partialUser = partial < User3 > type readonlyUser = Readonly < User3 > declare function f < T extends boolean > ( x : T ) : T extends true ? string : number ; const x3 = f ( Math . random ( ) < 0.5 ) const y3 = f ( false ) const z = f ( true ) Copy 条件类型 条件类型够表⽰⾮统⼀的类型,以⼀个条件表达式进⾏类型关系检测,从⽽在两种类型中选择其⼀:
上⾯的代码可以理解为: 若 T 能够赋值给 U ,那么类型是 X,否则为 Y ,有点类似于JavaScript中的 三元条件运算符.
⽐如声明⼀个函数 f ,它的参数接收⼀个布尔类型,当布尔类型为 true 时返回 string 类型,否 则返回 number 类型:
declare function f < T extends boolean > ( x : T ) : T extends true ? string : number ; const x = f ( Math . random ( ) < 0.5 ) const y = f ( false ) const z = f ( true ) Copy 条件类型就是这样,只有类型系统中给出充⾜的条件之后,它才会根据条件推断出类型结果.
条件类型与联合类型 条件类型有⼀个特性,就是「分布式有条件类型」,但是分布式有条件类型是有前提的,条件类型⾥待检 查的类型必须是 naked type parameter . naked type parameter 指的是裸类型参数 ,怎么理解?这个「裸」是指类型参数没有被包装在其 他类型⾥,⽐如没有被数组、元组、函数、Promise等等包裹.
type NakedUsage < T > = T extends boolean ? "YES" : "NO" type WrappedUsage < T > = [ T ] extends [ boolean ] ? "YES" : "NO" ; Copy 这⼀部分⽐较难以理解,可以把「分布式有条件类型」粗略得理解为类型版的 map()⽅法 ,然后我 们再看⼀些实⽤案例加深理解.
/ 裸类型参数 , 没有被任何其他类型包裹即 T type NakedUsage < T > = T extends boolean ? "YES" : "NO" type WrappedUsage < T > = [ T ] extends [ boolean ] ? "YES" : "NO" ; type Distributed = NakedUsage < number | boolean > type NotDistributed = WrappedUsage < number | boolean > type Diff < T , U > = T extends U ? never : T ; type R = Diff < "a" | "b" | "c" | "d" , "a" | "c" | "f" > ; type Temp2 = never | "b" | never | "d" type Filter < T , U > = T extends U ? T : never ; type R1 = Filter < string | number | ( ( ) => void ) , Function > ; type NonNullable2 < T > = Diff < T , null | undefined > ; type R2 = NonNullable2 < string | number | undefined > ; Copy 条件类型与映射类型 在⼀些有要求TS基础的公司,设计⼯具类型是⼀个⽐较⼤的考点.
interface Part { id : number ; name : string ; subparts : Part [ ] ; updatePart ( newName : string ) : void ; } interface Part2 { id2 : number ; name2 : string ; subparts3 : Part [ ] ; updatePart111 ( newName : string ) : void ; updatePart222 ( newName : string ) : void ; } type FunctionPropertyNames < T > = { [ K in keyof T ] : T [ K ] extends Function ? K : never } [ keyof T ] type R3 = FunctionPropertyNames < Part > ; type R89 = FunctionPropertyNames < Part2 > ; Copy 强⼤的infer关键字 infer 是⼯具类型和底层库中⾮常常⽤的关键字,表⽰在 extends 条件语句中待推断的类型变量,相对 ⽽⾔也⽐较难理解,我们不妨从⼀个 typescript ⾯试题开始: 之前学过 ReturnType ⽤于获取函数的返回类型,那么如何设计⼀个 ReturnType ?
infer ⾮常强⼤,由于它的存在可以做出⾮常多的骚操作. tuple转union,⽐如[string, number] -> string | number:
type ElementOf < T > = T extends Array < infer E > ? E : never ; type TTuple = [ string , number ] ; type ToUnion = ElementOf < ATuple > ; Copy class TestClass { constructor ( public name : string , public age : number ) { } } type ConstructorParameters5 < T extends new ( ... args : any [ ] ) => any > = T extends new ( ... args : infer P ) => any ? P : never ; type R4 = ConstructorParameters5 < typeof TestClass > Copy 常用的工具类型解读 ⽤ JavaScript 编写中⼤型程序是离不开 lodash 这种⼯具集的,⽽⽤ TypeScript 编程同样离不开类型 ⼯具的帮助,类型⼯具就是类型版的 lodash.
如下会介绍⼀些类型⼯具的设计与实现,如果项⽬不是⾮常简单的 demo 级项⽬,那么在开发过程中⼀定会⽤到它们。
起初,TypeScript 没有这么多⼯具类型,很多都是社区创造出来的,然后 TypeScript 陆续将⼀些常 ⽤的⼯具类型纳⼊了官⽅基准库内。
⽐如 ReturnType 、 Partial 、 ConstructorParameters 、 Pick 都是官⽅的内置⼯具类型. 其实上述的⼯具类型都可以被我们开发者⾃⼰模拟出来,本节学习⼀下如何设计⼯具类型.
可以把⼯具类型类⽐ js 中的⼯具函数,因此必须有输⼊和输出,⽽在TS的类型系统中能担当 类型⼊⼝的只有泛型.
⽐如 Partial ,它的作⽤是将属性全部变为可选.
type Partial < T > = { [ P in keyof T ] ? : T [ P ] } ; Copy 这个类型⼯具中,需要将类型通过泛型 T 传⼊才能对类型进⾏处理并返回新类型,可以说,⼀切类型 ⼯具的基础就是泛型.
类型递归 interface Company { id : number name : string } interface Person { id : number name : string adress : string company : Company } type R0 = Partial < Person > type DeepPartial < T > = { [ U in keyof T ] ? : T [ U ] extends object ? DeepPartial < T [ U ] > : T [ U ] } ; type R9 = DeepPartial < Person > Copy 关键字 像 keyof 、 typeof 这种常⽤关键字我们已经了解过了,当然还有很常⽤的 Type inference infer 关键字的使⽤,还有之前的 Conditional Type 条件类型,现在主要谈⼀下另外⼀些常⽤关键字. + - 这两个关键字⽤于映射类型中给属性添加修饰符,⽐如 -? 就代表将可选属性变为必选, - readonly 代表将只读属性变为⾮只读. ⽐如TS就内置了⼀个类型⼯具 Required<T>,它的作⽤是将传⼊的属性变为必选项:
type Required < T > = { [ P in keyof T ] - ? : T [ P ] } ; Copy 常⻅⼯具类型 Omit Omit 这个⼯具类型在开发过程中⾮常常⻅,以⾄于官⽅在3.5版本正式加⼊了 Omit 类型.
要了解之前先看⼀下另⼀个内置类型⼯具的实现 Exclude<T> :
type Exclude < T , U > = T extends U ? never : T ; type T = Exclude < 1 | 2 , 1 | 3 > Copy Exclude 的作⽤是从 T 中排除出可分配给 U 的元素. 这⾥的可分配即 assignable ,指可分配的, T extends U 指T是否可分配给U Omit = Exclude + Pick
type Omit < T , K > = Pick < T , Exclude < keyof T , K >> type Foo = Omit < { name : string , age : number } , 'name' > Copy Omit<T, K> 的作⽤是忽略 T 中的某些属性.
Merge Merge<O1, O2> 的作⽤是将两个对象的属性合并:
type O1 = { name : string id : number } type O2 = { id : number from : string } type R2 = Merge < O1 , O2 > Copy 这个类型⼯具也⾮常常⽤,他主要有两个部分组成:
Merge<O1, O2> = Compute<A> + Omit<U, T>
Compute 的作⽤是将交叉类型合并.即:
type Compute < A extends any > = A extends Function ? A : { [ K in keyof A ] : A [ K ] } type R1 = Compute < { x : 'x' } & { y : 'y' } > Copy Merge的最终实现如下:
type Merge < O1 extends object , O2 extends object > = Compute < O1 & Omit < O2 , keyof O1 >> Copy Intersection Intersection<T, U> 的作⽤是取 T 的属性,此属性同样也存在与 U.
type Props = { name : string ; age : number ; visible : boolean } ; type DefaultProps = { age : number } ; type DuplicatedProps = Intersection < Props , DefaultProps > ; Copy 实现
type Intersection < T extends object , U extends object > = Pick < T , Extract < keyof T , keyof U > & Extract < keyof U , keyof T > > ; Copy Overwrite Overwrite<T, U>顾名思义,是⽤ U 的属性覆盖 T 的相同属性.
type Props = { name : string ; age : number ; visible : boolean } ; type NewProps = { age : string ; other : string } ; type ReplacedProps = Overwrite < Props , NewProps > Copy 即:
type Overwrite < T extends object , U extends object , I = Diff < T , U > & Intersection < U , T > > = Pick < I , keyof I > ; Copy Mutable 将 T 的所有属性的 readonly 移除
type Mutable < T > = { - readonly [ P in keyof T ] : T [ P ] } Copy Record Record 允许从 Union 类型中创建新类型,Union 类型中的值⽤作新类型的属性。
type Car = 'Audi' | 'BMW' | 'MercedesBenz' type CarList = Record < Car , { age : number } > const cars : CarList = { Audi : { age : 119 } , BMW : { age : 113 } , MercedesBenz : { age : 133 } , } Copy 在实战项⽬中尽量多⽤ Record,它会帮助规避很多错误,在 vue 或者 react 中有很多场景选择 Record 是更优解。
巧⽤类型约束 在 .tsx ⽂件⾥,泛型可能会被当做 jsx 标签
const toArray = <T>(element: T) => [element]; // Error in .tsx file. Copy 加 extends 可破
const toArray = < T extends { } > ( element : T ) => [ element ] ; Copy 模块与命名空间 模块系统 TypeScript 与 ECMAScript 2015 ⼀样,任何包含顶级 import 或者 export 的⽂件都被当成⼀个 模块。
相反地,如果⼀个⽂件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可⻅的。
模块语法 可以⽤ export 关键字导出变量或者类型,⽐如:
export const a = 1 export type Person = { name : String } Copy 如果想⼀次性导出,那么你可以:
const a = 1 type Person = { name : String } export { a , Person } import { a , Person } from './export' ; Copy 同样的也可以重命名导⼊的模块:
import { Person as P } from './export' ; Copy 如果不想⼀个个导⼊,想把模块整体导⼊,可以这样:
import * as P from './export' ; Copy 甚⾄可以导⼊后导出模块:
export { Person as P } from './export' ; Copy 当然,除了上⾯的⽅法之外还有默认的导⼊导出:
export default ( a = 1 ) export default ( ) => 'function' Copy 命名空间 命名空间⼀个最明确的⽬的就是解决重名问题。
TypeScript 中命名空间使⽤ namespace 来定义,语法格式如下:
namespace SomeNameSpaceName { export interface ISomeInterfaceName { } export class SomeClassName { } } Copy 以上定义了⼀个命名空间 SomeNameSpaceName,如果需要在外部可以调⽤ SomeNameSpaceName 中的类和接⼝,则需要在类和接⼝添加 export 关键字. 其实⼀个 命名空间 本质上⼀个 对象 ,它的作⽤是将⼀系列相关的全局变量组织到⼀个对象的属性 你在⼿动构建⼀个命名空间,但是在 ts 中, namespace 提供了⼀颗语法糖。上述可⽤语法糖改写 成:
namespace Letter { export let a = 1 ; export let b = 2 ; export let c = 3 ; export let z = 26 ; Copy 编辑成 js :
var Letter ; ( function ( Letter ) { Letter . a = 1 ; Letter . b = 2 ; Letter . c = 3 ; Letter . z = 26 ; } ) ( Letter || ( Letter = { } ) ) ; Copy 命名空间的⽤处 命名空间在现代TS开发中的重要性并不⾼,主要原因是ES6引⼊了模块系统,⽂件即模块的⽅式使得开发 者能更好的得组织代码,但是命名空间并⾮⼀⽆是处,通常在⼀些⾮ TypeScript 原⽣代码的 .d.ts ⽂ 件中使⽤,主要是由于 ES Module 过于静态,对 JavaScript 代码结构的表达能⼒有限。 因此在正常的TS项⽬开发过程中并不建议⽤命名空间。
使⽤第三⽅ d.ts Github 上有⼀个库 DefinitelyTyped 它定义了市⾯上主流的JavaScript 库的 d.ts ,⽽且我们可以很⽅ 便地⽤ npm 引⼊这些 d.ts。
编写 d.ts ⽂件 关键字 declare 表⽰声明的意思,我们可以⽤它来做出各种声明:
declare var 声明全局变量 declare function 声明全局⽅法 declare class 声明全局类 declare enum 声明全局枚举类型 declare namespace 声明(含有⼦属性的)全局对象 interface 和 type 声明全局类型 TypeScript 的编译原理 编译器的组成 TypeScript有⾃⼰的编译器,这个编译器主要有以下部分组成:
Scanner 扫描器 Parser 解析器 Binder 绑定器 Emitter 发射器 Checker 检查器 编译器的处理 扫描器通过扫描源代码⽣成token流:
SourceCode(源码)+ 扫描器 --> Token 流
解析器将token流解析为抽象语法树(AST):
Token 流 + 解析器 --> AST(抽象语法树)
绑定器将AST中的声明节点与相同实体的其他声明相连形成符号(Symbols),符号是语义系统的主要构 造块:
AST + 绑定器 --> Symbols(符号)
检查器通过符号和AST来验证源代码语义:
AST + 符号 + 检查器 --> 类型验证
最后我们通过发射器⽣成JavaScript代码:
AST + 检查器 + 发射器 --> JavaScript 代码
编译器处理流程 TypeScript 的编译流程也可以粗略得分为三步:
结合上部分的编译器各个组成部分,流程如下图:
验证成果 一道题
题目地址