交叉類型
交叉類型將多個類型合并為一個類型,相當于新類型具有這多個類型的所有特性,相當于是一種并的操作,通常在使用混入(mixin)的場合使用交叉類型,交叉類型的形式如:
T & U
示例:
interface IPerson {
name: string;
age: number;
}
interface IMan {
love: string;
age: number;
}
let mixin: <T, U>(age: T, love: U) => T = function<T, U>(age: T, love: U): T & U {
return Object.assign(age, love);
}
let me = mixin<IPerson, IMan>({name: 'funlee', age: 10}, { love: 'TS', age: 18});
console.log(me); // {name: "funlee", age: 18, love: 'TS}
聯合類型
聯合類型用于限制傳入的值的類型只能是 | 分隔的每個類型,如:number | string | boolean
表示一個值的類型只能是 number、string、boolean 中的一種。
此外,如果一個值是聯合類型,那么我們只能訪問它們中共有的部分(共有的屬性與方法),即相當于一種交的關系,如:
interface IPerson {
name: string;
age: number;
}
interface IMan {
love: string;
age: number;
}
let me: IPerson | IMan;
me = {
name: 'funlee',
age: 18,
love: 'TS'
}
console.log(me); // {name: "funlee", age: 18, love: "TS"}
console.log(me.name); // ERROR
console.log(me.age); // 18
console.log(me.love); // ERROR
類型保護與區分類型
聯合類型可以讓一個值可以為不同的類型,但隨之帶來的問題就是訪問非共同方法時會報錯。那么該如何區分值的具體類型,以及如何訪問共有成員?
1.使用類型斷言
interface IPerson {
name: string;
age: number;
}
interface IMan {
love: string;
age: number;
}
let me: IPerson | IMan;
me = {
name: 'funlee',
age: 18,
love: 'TS'
}
console.log(me); // {name: "funlee", age: 18, love: "TS"}
if((me as IPerson).name) {
console.log((me as IPerson).name); // funlee
}
if((me as IMan).love) {
console.log((me as IMan).love); // TS
}
2.使用類型保護
為了避免像上例那樣寫一堆類型斷言,我們可以使用類型保護,如寫一個類型判斷函數:
function isIinterface(obj: IPerson | IMan): obj is IPerson {
return (obj as IPerson).name !== undefined;
}
這種 param is SomeType
的形式,就是類型保護,我們可以用它來明確一個聯合類型變量的具體類型,在調用時 TypeScript 就會將變量縮減為該具體類型,如此一來以下調用就是合法的了:
interface IPerson {
name: string;
age: number;
}
interface IMan {
love: string;
age: number;
}
let me: IPerson | IMan;
me = {
name: 'funlee',
age: 18,
love: 'TS'
}
function isIPerson(obj: IPerson | IMan): obj is IPerson {
return (obj as IPerson).name !== undefined;
}
function isIMan(obj: IPerson | IMan): obj is IMan {
return (obj as IMan).love !== undefined;
}
console.log(me); // {name: "funlee", age: 18, love: "TS"}
if(isIPerson(me)) {
console.log(me.name); // funlee
}
if(isIMan(me)) {
console.log(me.love); // TS
}
3.typeof 和 instanceof
當我們使用了 typeof 和 instanceof 后,TypeScript 就會自動限制類型為某一具體類型,從而我們可以安全地在語句體內使用具體類型的方法和屬性。
function show(param: number | string) {
if (typeof param === 'number') {
console.log(`${param} is number`)
} else {
console.log(`${param} is string`)
}
}
typeof 用于基本數據類型,instanceof 用于引用類型,對于類,我們則可以使用 instanceof,如:
class Person {
name: string = 'funlee';
age: number = 18;
}
class Man {
age: number = 12;
love: string = 'TS';
}
let me: Person | Man;
me = Math.random() < 0.5 ? new Person() : new Man();
if(me instanceof Person) {
console.log(me.name);
}
if(me instanceof Man) {
console.log(me.love);
}
null 的類型
null 和 undefined 可以賦給任何的類型,因為它們是所有其他類型的一個有效值,如:
let x1: number = null
let x2: string = null
let x3: boolean = null
let x4: undefined = null
let y1: number = undefined
let y2: string = undefined
let y3: boolean = undefined
let y4: null = undefined
在 TypeScript里,我們可以使用 --strictNullChecks
標記,開啟這個標記后,當我們聲明一個變量時,就不會自動包含 null 或 undefined,如:
// 開啟`--strictNullChecks`后
// Type 'null' is not assignable to type 'number'.
let x1: number = null
// Type 'null' is not assignable to type 'string'.
let x2: string = null
// Type 'null' is not assignable to type 'boolean'.
let x3: boolean = null
// Type 'null' is not assignable to type 'undefined'.
let x4: undefined = null
// Type 'undefined' is not assignable to type 'number'.
let y1: number = undefined
// Type 'undefined' is not assignable to type 'string'.
let y2: string = undefined
// Type 'undefined' is not assignable to type 'boolean'.
let y3: boolean = undefined
// Type 'undefined' is not assignable to type 'null'.
let y4: null = undefined
但是我們可以手動使用聯合類型來明確包含,如:
et x = 123
x = null // 報錯
let y: number | null = 123
y = null // 允許
y = undefined // 報錯,`undefined`不能賦值給`number | null`
當開啟了 --strictNullChecks
,可選參數/屬性就會被自動地加上 | undefined,如:
function foo(x: number, y?: number) {
return x + (y || 0)
}
foo(1, 2) // 允許
foo(1) // 允許
foo(1, undefined) // 允許
foo(1, null) // 報錯,不允許將null賦值給`number | undefined`類型
類型別名
類型別名可以給現有的類型起個新名字,它和接口很像但又不一樣,因為類型別名可以作用于原始值、聯合類型、元組及其他任何需要手寫的了類型,語法如:
type 新名字 = 已有類型
如:type Name = string
別名不會新建一個類型,它只會創建一個新的名字來引用現有類型。
泛型別名
別名支持泛型。
type Container<T> = {
value: T
}
let name: Container<string> = {
value: 'funlee'
}
但是類型別名不能出現在聲明右側的任何地方,如:
type Alias = Array<Alias> // 報錯,別名Alias循環引用了自身
和接口的區別
- 錯誤信息、鼠標懸停時,不會使用別名,而是直接顯示為所引用的類型
- 別名不能被 extends 和 implements
字符串字面量類型
字符串字面量類型允許我們定義一個別名,類型為別名的變量只能取固定的幾個值,如:
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
let x1: Easing = 'uneasy' // 報錯: Type '"uneasy"' is not assignable to type 'Easing'
let x2: Easing = 'ease-in' // 允許
字符串字面量類型還能用于區分函數重載,如:
function createElement(tagName: 'img'): HTMLImageElement
function createElement(tagName: 'input'): HTMLInputElement
// ... 其他重載函數
function createElement(tagName: string): Element {
// ...
}
可辨識聯合
可以合并字符串字面量類型、聯合類型、類型保護和類型別名來創建可辨識聯合的高級模式(也稱為標簽聯合或者代數數據類型),具有3個要素:
- 具有普通的字符串字面量屬性——可辨識的特征
- 一個類型別名,用來包含了那些類型的聯合——聯合
- 此屬性上的類型保護
創建一個可辨識聯合類型,首先需要聲明將要聯合的接口,每個接口都要有一個可辨識的特征,如(kind屬性):
interface Square {
kind: 'square'
size: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
interface Circle {
kind: 'circle'
radius: number
}
現在,各個接口之間還是沒有關聯的,所以我們需要使用類型別名來聯合這幾個接口,如
type Shape = Square | Rectangle | Circle;
現在,使用可辨識聯合,如:
function area(s: Shape) {
switch (s.kind) {
case 'square':
return s.size * s.size
case 'rectangle':
return s.height * s.width
case 'circle':
return Math.PI * s.radius ** 2
}
}
多態的 this
多態的 this 類型表示的是某個包含類或接口的子類型,例子如:
class BasicCalculator {
public constructor(protected value: number = 0) {
}
public currentValue(): number {
return this.value
}
public add(operand: number): this {
this.value += operand
return this
}
public multiply(operand: number): this {
this.value *= operand
return this
}
}
let v = new BasicCalculator(2).multiply(5).add(1).currentValue() // 11
由于使用了 this 類型,當子類繼承父類的時候,新的類就可以直接使用之前的方法,而不需要做任何的改變,如:
class ScientificCalculator extends BasicCalculator {
public cconstructor(value = 0) {
super(value)
}
public sin() {
this.value = Math.sin(this.value)
return this
}
}
let v = new BasicCalculator(2).multiply(5).sin().add(1).currentValue();
如果沒有 this 類型,那么 ScientificCalculator 就不能夠在繼承 BasicCalculator 的同時還保持接口的連貫性。因為m ultiply 方法會返回 BasicCalculator 類型,而BasicCalculator 沒有 sin 方法。然而,使用 this 類型,multiply 就會返回 this,在這里就是 ScientificCalculator。
索引類型
索引類型能使編譯器能夠檢查使用了動態屬性名的代碼,如:
我們想要完成一個函數,它可以選取對象中的部分元素的值,那么:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n])
}
interface Person {
name: string
age: number
}
let p: Person = {
name: 'funlee',
age: 21
}
let res = pluck(p, ['name']) // 允許
以上代碼解釋如下:
- 首先,使用
keyof
關鍵字,它是索引類型查詢操作符,它能夠獲得任何類型 T 上已知的公共屬性名的聯合。如例子中,keyof T
相當于'name' | 'age'
- 然后,
K extends keyof T
表明 K 的取值限制于'name' | 'age'
- 而
T[K]
則代表對象里相應 key 的元素的類型,所以在例子中,p 對象里的 name 屬性,是 string 類型,所以此時 T[K] 相當于Person[name]
,即相當于類型 string,所以返回的是 string[],所以 res 的類型為 string[]
所以,根據以上例子,舉一反三有:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
let obj = {
name: 'funlee',
age: 21,
male: true
}
let x1 = getProperty(obj, 'name') // 允許,x1的類型為string
let x2 = getProperty(obj, 'age') // 允許,x2的類型為number
let x3 = getProperty(obj, 'male') // 允許,x3的類型為boolean
let x4 = getProperty(obj, 'hobby') // 報錯:Argument of type '"hobby"' is not assignable to parameter of type '"name" | "age" | "male"'.
索引類型和字符串索引簽名
keyof 和 T[K] 與字符串索引簽名進行交互,如果有一個帶有字符串索引簽名的類型,那么 keyof T 為 string,且 T[string] 為索引簽名的類型,如:
interface Demo<T> {
[key: string]: T
}
let keys: keyof Demo<boolean> // keys的類型為string
let value: Demo<number>['foo'] // value的類型為number
映射類型
我們可能會遇到這么一些需求:
- 將一個現有類型的每個屬性都變為可選的,如:
interface IPerson {
name: string
age: number
}
可選版本為:
interface PersonPartial {
name?: string
age?: number
}
- 或者將每個屬性都變為只讀的,如:
interface IPersonReadonly {
readonly name: string
readonly age: number
}
而現在 typeScript 為我們提供了映射類型,能夠使得這種轉化更加方便,在映射類型里,新類型將以相同的形式去轉換舊類型里每個屬性,如以上例子可以改寫為:
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type Partial<T> = {
[P in keyof T]?: T[P]
}
type PersonReadonly = Readonly<Person>
type PersonPartial = Partial<Person>
我們還可以寫出更多的通用映射類型,如:
// 可為空類型
type Nullable<T> {
[P in keyof T]: T[P] | null
}
// 包裝一個類型的屬性
type Proxy<T> = {
get(): T
set(value: T): void
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>
}
function proxify(o: T): Proxify<T> {
// ...
}
let proxyProps = proxify(props)
由映射類型進行推斷(拆包)
上面展示了如何包裝一個類型,那么與之相反的就有拆包操作,示例如:
function unproxify<T>(t: Proxify<T>): T {
let result = <T>{}
for (const k in t) {
result[k] = t[k].get()
}
return result
}
let originalProps = unproxify(proxyProps);