背景
大家用過 Typescript
都清楚,很多時候我們需要提前宣告一個型別,再將型別賦予變數。
例如在業務中,我們需要渲染一個表格,往往需要定義:
interface Row {
user: string
email: string
id: number
vip: boolean
// ...
}
const tableDatas: Row[] = []
// ...
有時候我們也需要表格對應的搜尋表單,需要其中一兩個搜尋項,如果剛接觸 typescript 的同學可能會立刻這樣寫:
interface SearchModel {
user?: string
id?: number
}
const model: SearchModel = {
user: '',
id: undefined
}
這樣寫會出現一個問題,如果後面id 型別要改成 string
,我們需要改 2 處地方,不小心的話可能就會忘了改另外一處。所以,有些人會這樣寫:
interface SearchModel {
user?: Row['user']
id?: Row['id']
}
這固然是一個解決方法,但事實上,我們前面已經定義了 Row
型別,這其實是可以更優雅地複用的:
const model: Partial<Row> = {
user: '',
id: undefined
}
// 或者需要明確指定 key 的,可以
const model2: Partial<Pick<Row, 'user'|'id'>>
這樣一來,很多情況下,我們可以儘量少地寫重複的型別,複用已有型別,讓程式碼更加優雅容易維護。
上面使用到的 Partial
和 Pick
都是 typescript 內建的型別別名。下面給大家介紹一下 typescript 常用的內建型別,以及自行擴充的型別。
typescript 內建型別
Partial<T>
將型別 T 的所有屬性標記為可選屬性
type Partial<T> = {
[P in keyof T]?: T[P];
};
使用場景:
// 賬號屬性
interface AccountInfo {
name: string
email: string
age: number
vip: 0|1 // 1 是vip ,0 是非vip
}
// 當我們需要渲染一個賬號表格時,我們需要定義
const accountList: AccountInfo[] = []
// 但當我們需要查詢過濾賬號資訊,需要通過表單,
// 但明顯我們可能並不一定需要用到所有屬性進行搜尋,此時可以定義
const model: Partial<AccountInfo> = {
name: '',
vip: undefind
}
Required<T>
與 Partial 相反,Required 將型別 T 的所有屬性標記為必選屬性
type Required<T> = {
[P in keyof T]-?: T[P];
};
Readonly<T>
將所有屬性標記為 readonly, 即不能修改
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Pick<T, K>
從 T 中過濾出屬性 K
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
使用場景:
interface AccountInfo {
name: string
email: string
age: number
vip?: 0|1 // 1 是vip ,0 是非vip
}
type CoreInfo = Pick<AccountInfo, 'name' | 'email'>
/*
{
name: string
email: stirng
}
*/
Record<K, T>
標記物件的 key value型別
type Record<K extends keyof any, T> = {
[P in K]: T;
};
使用場景:
// 定義 學號(key)-賬號資訊(value) 的物件
const accountMap: Record<number, AccountInfo> = {
10001: {
name: 'xx',
email: 'xxxxx',
// ...
}
}
const user: Record<'name'|'email', string> = {
name: '',
email: ''
}
// 複雜點的型別推斷
function mapObject<K extends string | number, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>
const names = { foo: "hello", bar: "world", baz: "bye" };
// 此處推斷 K, T 值為 string , U 為 number
const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
Exclude<T, U>,Omit<T, K>
移除 T 中的 U 屬性
type Exclude<T, U> = T extends U ? never : T;
使用場景:
// 'a' | 'd'
type A = Exclude<'a'|'b'|'c'|'d' ,'b'|'c'|'e' >
乍一看好像這個沒啥卵用,但是,我們通過一番操作,之後就可以得到 Pick
的反操作:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type NonCoreInfo = Omit<AccountInfo, 'name' | 'email'>
/*
{
age: number
vip: 0|1,
}
*/
Extract<T, U>
Exclude
的反操作,取 T,U兩者的交集屬性
type Extract<T, U> = T extends U ? T : never;
使用 demo:
// 'b'|'c'
type A = Extract<'a'|'b'|'c'|'d' ,'b'|'c'|'e' >
這個看起來沒啥用,實際上還真沒啥卵用,應該是我才疏學淺,還沒發掘到其用途。
NonNullable<T>
排除型別 T 的 null
| undefined
屬性
type NonNullable<T> = T extends null | undefined ? never : T;
使用 demo
type A = string | number | undefined
type B = NonNullable<A> // string | number
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
let s1: string = x; // Error, x 可能為 undefined
let s2: string = y; // Ok
}
Parameters<T>
獲取一個函式的所有引數型別
// 此處使用 infer P 將引數定為待推斷型別
// T 符合函式特徵時,返回引數型別,否則返回 never
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
使用demo:
interface IFunc {
(person: IPerson, count: number): boolean
}
type P = Parameters<IFunc> // [IPerson, number]
const person01: P[0] = {
// ...
}
另一種使用場景是,快速獲取未知函式的引數型別
import {somefun} from 'somelib'
// 從其他庫匯入的一個函式,獲取其引數型別
type SomeFuncParams = Parameters<typeof somefun>
// 內建函式
// [any, number?, number?]
type FillParams = Parameters<typeof Array.prototype.fill>
ConstructorParameters<T>
類似於 Parameters<T>
, ConstructorParameters 獲取一個類的建構函式引數
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
使用 demo:
// string | number | Date
type DateConstrParams = ConstructorParameters<typeof Date>
ReturnType<T>
獲取函式型別 T 的返回型別
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
使用方式和 Parameters<T>
類似,不再贅述
InstanceType<T>
獲取一個類的返回型別
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
使用方式和 ConstructorParameters<T>
類似,不再贅述
自定義常用型別
Weaken
使用 typescript
有時候需要重寫一個庫提供的 interface 的某個屬性,但是重寫 interface
有可能會導致衝突:
interface Test {
name: string
say(word: string): string
}
interface Test2 extends Test{
name: Test['name'] | number
}
// error: Type 'string | number' is not assignable to type 'string'.
那麼可以通過一些 type 來曲線救國實現我們的需求:
// 原理是,將 型別 T 的所有 K 屬性置為 any,
// 然後自定義 K 屬性的型別,
// 由於任何型別都可以賦予 any,所以不會產生衝突
type Weaken<T, K extends keyof T> = {
[P in keyof T]: P extends K ? any : T[P];
};
interface Test2 extends Weaken<Test, 'name'>{
name: Test['name'] | number
}
// ok
陣列 轉換 成 union
有時候需要
const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const; // TS 3.4
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number]; // union type : 'hearts' | 'diamonds' | 'spades' | 'clubs'
根據 enum 生成 union
-
enum 的 key 值 union
enum Weekday { Mon = 1 Tue = 2 Wed = 3 } type WeekdayName = keyof typeof Weekday // 'Mon' | 'Tue' | 'Wed'
-
enum 無法實現value-union , 但可以 object 的 value 值 union
const lit = <V extends keyof any>(v: V) => v; const Weekday = { MONDAY: lit(1), TUESDAY: lit(2), WEDNESDAY: lit(3) } type Weekday = (typeof Weekday)[keyof typeof Weekday] // 1|2|3
PartialRecord
前面我們講到了 Record 型別,我們會常用到
interface Model {
name: string
email: string
id: number
age: number
}
// 定義表單的校驗規則
const validateRules: Record<keyof Model, Validator> = {
name: {required: true, trigger: `blur`},
id: {required: true, trigger: `blur`},
email: {required: true, message: `...`},
// error: Property age is missing in type...
}
這裡出現了一個問題,validateRules
的 key 值必須和 Model
全部匹配,缺一不可,但實際上我們的表單可能只有其中的一兩項,這時候我們就需要:
type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>
const validateRules: PartialRecord<keyof Model, Validator> = {
name: {required: true, trigger: `blur`}
}
這個例子組合使用了 typescript
內建的 型別別名 Partial
和 Partial
。
Unpacked
解壓抽離關鍵型別
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
總結
事實上,基於已有的型別別名,還有新推出的 infer
待推斷型別,可以探索出各種各樣的複雜組合玩法,這裡不再多說,大家可以慢慢探索。
感謝閱讀!
本文首發於 github 部落格
如文章對你有幫助,你的 star 是對我最大的支援
插播廣告:
深圳 Shopee 長期內推
崗位:前端,後端(要轉go),產品,UI,測試,安卓,IOS,運維 全都要。
薪酬福利:20K-50K?,7點下班?(劃重點),免費水果?,免費晚餐?,15天年假?,14天帶薪病假。 點選檢視詳情
簡歷發郵箱:chenweiyu6909@gmail.com
或者加我微信:cwy13920,實時反饋面試進度哦。