你可能不知道的typescript實用小技巧

前言

用瞭很久的 typescript,用瞭但感覺又沒完全用。因為很多 typescript 的特性沒有被使用,查看之前寫的代碼滿屏的 any,這樣就容易導致很多 bug,也沒有發揮出 typescript 真正的“類型”威力。本文總結瞭一些使用 typescript 的小技巧,以後使用 typescript 時可以運用起來。

廢話不多說,直接上代碼。

函數重載

當希望傳 user 參數時,不傳 flag,傳 para 時,傳 flag。就可以這樣寫:

interface User {
  name: string;
  age: number;
}

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {

  public test(para: User): number;
  public test(para: number, flag: boolean): number;

  public test(para: User | number, flag?: boolean): number {
    // 具體實現
    return 1;
  }
}

const someClass = new SomeClass();

// ok
someClass.test(user);
someClass.test(123, false);

// Error
// someClass.test(123); 
//Argument of type 'number' is not assignable to parameter of type 'User'.
// someClass.test(user, false);
//Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'number'.

映射類型

在瞭解映射類型之前,需要瞭解 keyof, never, typeof, in。

keyof:keyof 取 interface 的鍵

interface Point {
    x: number;
    y: number;
}

// type keys = "x" | "y"
type keys = keyof Point;

never:永遠不存在的值的類型

官方描述:

the never type represents the type of values that never occur.

// 例子:進行編譯時的全面的檢查
type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 這裡 foo 被收窄為 string 類型
  } else if (typeof foo === "number") {
    // 這裡 foo 被收窄為 number 類型
  } else {
    // foo 在這裡是 never
    const check: never = foo;
  }
}

使用 never 避免出現新增瞭聯合類型沒有對應的實現,目的就是寫出類型絕對安全的代碼。

typeof:取某個值的 type

const a: number = 3

// 相當於: const b: number = 4
const b: typeof a = 4

in:檢查一個對象上是否存在一個屬性

interface A {
  x: number;
}

interface B {
  y: string;
}

function doStuff(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

映射類型就是將一個類型映射成另外一個類型,簡單理解就是新類型以相同的形式去轉換舊類型的每個屬性。

Partial, Readonly, Nullable, Required

  • Partial 將每個屬性轉換為可選屬性
  • Readonly 將每個屬性轉換為隻讀屬性
  • Nullable 轉換為舊類型和null的聯合類型
  • Required 將每個屬性轉換為必選屬性
type Partial<T> = {
    [P in keyof T]?: T[P];
}

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

type Nullable<T> = { 
  [P in keyof T]: T[P] | null 
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

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

type PersonPartial = Partial<Person>;
type PersonReadonly = Readonly<Person>;
type PersonNullable = Nullable<Person>;

type PersonPartial = {
    name?: string | undefined;
    age?: number | undefined;
}

type PersonReadonly = {
    readonly name: string;
    readonly age: number;
}

type PersonNullable = {
      name: string | null;
      age: number | null;
}

interface Props {
  a?: number;
  b?: string;
}

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 };
// Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Pick, Record

  • Pick 選取一組屬性指定新類型
  • Record 創建一組屬性指定新類型,常用來聲明普通Object對象
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

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

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

todo; // = const todo: TodoPreview


interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const nav: Record<Page, PageInfo> = {
  about: { title: "title1" },
  contact: { title: "title2" },
  home: { title: "title3" },
};

nav.about; // = const nav: Record

Exclude, Omit

  • Exclude 去除交集,返回剩餘的部分
  • Omit 適用於鍵值對對象的Exclude,去除類型中包含的鍵值對
type Exclude<T, U> = T extends U ? never : T
type Omit = Pick<T, Exclude<keyof T, K>>

// 相當於: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "a",
  completed: false,
};

ReturnType

獲取返回值類型,一般為函數

type ReturnType<T extends (...args: any) => any>
  = T extends (...args: any) => infer R ? R : any;

declare function f1(): { a: number; b: string };
type T1 = ReturnType<typeof f1>;
//    type T1 = {
//        a: number;
//        b: string;
//    }

還有很多映射類型,可查看Utility Types參考。

類型斷言

類型斷言用來明確的告訴 typescript 值的詳細類型,合理使用能減少我們的工作量。

比如一個變量並沒有初始值,但是我們知道它的類型信息(它可能是從後端返回)有什麼辦法既能正確推導類型信息,又能正常運行瞭?有一種網上的推薦方式是設置初始值,然後使用 typeof 拿到類型(可能會給其他地方用)。也可以使用類型斷言可以解決這類問題:

interface User { 
    name: string; 
    age: number; 
}

export default class someClass { 
    private user = {} as User;
} 

枚舉

枚舉類型分為數字類型與字符串類型,其中數字類型的枚舉可以當標志使用:

enum AnimalFlags {
    None = 0, 
    HasClaws = 1 << 0, 
    CanFly = 1 << 1, 
    HasClawsOrCanFly = HasClaws | CanFly 
}

interface Animal { 
    flags: AnimalFlags; 
   [key: string]: any; 
}

function printAnimalAbilities(animal: Animal) { 
    var animalFlags = animal.flags; 
    if (animalFlags & AnimalFlags.HasClaws) { 
        console.log('animal has claws'); 
    } 
    if (animalFlags & AnimalFlags.CanFly) { 
        console.log('animal can fly'); 
    } 
    if (animalFlags == AnimalFlags.None) { 
        console.log('nothing'); 
    } 
}

var animal = { flags: AnimalFlags.None }; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // animal has claws 
animal.flags &= ~AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly; 
printAnimalAbilities(animal); // animal has claws, animal can fly 

  • 使用 |= 來添加一個標志;
  • 組合使用 &= 和 ~ 來清理一個標志;
  • | 來合並標志。

這個或許不常用,在 typescript 關於 types 源碼中我們也可以看到類似的代碼:

字符串類型的枚舉可以維護常量:

const enum TODO_STATUS {
  TODO = 'TODO',
  DONE = 'DONE',
  DOING = 'DOING'
}

function todos (status: TODO_STATUS): Todo[];

todos(TODO_STATUS.TODO)

元組

表示一個已知元素數量和類型的數組,各元素的類型不必相同。

let x: [string, number];
x = ['hello', 10]; 

在發出不固定多個請求時,可以應用:

const requestList: any[] = [http.get<A>('http://some.1')]; // 設置為 any[] 類型 
if (flag) { 
    requestList[1] = (http.get<B>('http://some.2')); 
} 
const [ { data: a }, response ] = await Promise.all(requestList) as [Response<A>, Response<B>?]

范型

在定義泛型後,有兩種方式使用,一種是傳入泛型類型,另一種使用類型推斷。

declare function fn<T>(arg: T): T; // 定義一個泛型函數 
const fn1 = fn<string>('hello'); // 第一種方式,傳入泛型類型 
string const fn2 = fn(1); // 第二種方式,從參數 arg 傳入的類型 number,來推斷出泛型 T 的類型是 number 

一個扁平數組結構建樹形結構例子:

// 轉換前數據 
const arr = [ 
{ id: 1, parentId: 0, name: 'test1'}, 
{ id: 2, parentId: 1, name: 'test2'}, 
{ id: 3, parentId: 0, name: 'test3'} 
]; 
// 轉化後 
[ { id: 1, parentId: 0, name: 'test1', 
    childrenList: [ { id: 2, parentId: 1, name: 'test2', childrenList: [] } ] }, 
    { id: 3, parentId: 0, name: 'test3', childrenList: [] } 
]


interface Item { 
    id: number; 
    parentId: number; 
    name: string; 
}

// 傳入的 options 參數中,得到 childrenKey 的類型,然後再傳給 TreeItem

interface Options<T extends string> { 
    childrenKey: T; 
} 
type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] }; 
declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[]; 
listToTree(arr, { childrenKey: 'childrenList' }).forEach(i => i.childrenList) 

infer

表示在 extends 條件語句中待推斷的類型變量。

type ParamType<T> = T extends (param: infer P) => any ? P : T; 

這句話的意思是:如果 T 能賦值給 (param: infer P) => any,則結果是 (param: infer P) => any 類型中的參數 P,否則返回為 T。

interface User { 
    name: string; 
    age: number; 
} 
type Func = (user: User) => void 
type Param = ParamType<Func>; // Param = User 
type AA = ParamType<string>; // string

例子:

// [string, number] -> string | number
type ElementOf<T> = T extends Array<infer E> ? E : never;

type TTuple = [string, number];

type ToUnion = ElementOf<TTuple>; // string | number


// T1 | T2 -> T1 & T2
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Result = UnionToIntersection<T1 | T2>; // T1 & T2

總結

typescript 關於類型限制還是非常強大的,由於文章有限,還有其他類型比如聯合類型,交叉類型等讀者可自行翻閱資料查看。剛開始接觸范型以及其各種組合會感覺不熟練,接下來在項目中會慢慢應用,爭取將 bug 降至最低限度。

到此這篇關於typescript實用小技巧的文章就介紹到這瞭,更多相關typescript實用小技巧內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: