在我們探索 TypeScript 開發的第二部分中,我們引入了另外十種自訂實用程式類型,這些類型可以擴展程式碼的功能,提供更多工具來更有效地管理類型。這些實用程式類型有助於保持您的程式碼庫乾淨、高效和健壯。

第一部分: TypeScript 專案的 1-10 個自訂實用程式類型

總有機碳

<a name="NonNullableDeep"></a>

NonNullableDeep

NonNullableDeep類型是一個實用程序,可從給定類型T的所有屬性中深度刪除nullundefined 。這意味著不僅物件的頂級屬性不可為空,而且所有嵌套屬性也遞歸地標記為不可為空。在必須確保物件的屬性(包括深度嵌套的屬性)不為nullundefined情況下(例如在處理必須完全填充的資料時),此類型特別有用。

type NonNullableDeep<T> = {
  [P in keyof T]: NonNullable<T[P]> extends object 
    ? NonNullableDeep<NonNullable<T[P]>> 
    : NonNullable<T[P]>;
};

例子

以下範例示範如何套用NonNullableDeep類型來確保Person物件本身及其任何巢狀屬性都無法為nullundefined ,從而確保整個物件已完全填入。

interface Address {
  street: string | null;
  city: string | null;
}

interface Person {
  name: string | null;
  age: number | null;
  address: Address | null;
}

const person: NonNullableDeep<Person> = {
  name: "Anton Zamay",
  age: 26,
  address: {
    street: "Secret Street 123",
    city: "Berlin",
  },
};

// Error: Type 'null' is not assignable to type 'string'.
person.name = null;
// Error: Type 'undefined' is not assignable to type 'number'.
person.age = undefined;
// Error: Type 'null' is not assignable to type 'Address'.
person.address = null;
// Error: Type 'null' is not assignable to type 'string'.
person.address.city = null;

<a name="Merge"></a>

Merge

Merge<O1, O2>類型對於透過組合兩個物件類型O1O2的屬性來建立新類型非常有用。當屬性重疊時, O2中的屬性將覆寫O1中的屬性。當您需要擴展或自訂現有類型以確保特定屬性優先時,這特別有用。

type Merge<O1, O2> = O2 & Omit<O1, keyof O2>;

例子

在此範例中,我們定義了兩種物件類型,分別表示預設設定和使用者設定。使用Merge類型,我們組合這些設定來建立最終配置,其中userSettings會覆蓋defaultSettings

type DefaultSettings = {
  theme: string;
  notifications: boolean;
  autoSave: boolean;
};

type UserSettings = {
  theme: string;
  notifications: string[];
  debugMode?: boolean;
};

const defaultSettings: DefaultSettings = {
  theme: "light",
  notifications: true,
  autoSave: true,
};

const userSettings: UserSettings = {
  theme: "dark",
  notifications: ["Warning 1", "Error 1", "Warning 2"],
  debugMode: true,
};

type FinalSettings = Merge<DefaultSettings, UserSettings>;

const finalSettings: FinalSettings = {
  ...defaultSettings,
  ...userSettings
};

<a name="TupleToObject"></a>

TupleToObject

TupleToObject類型是將元組類型轉換為物件類型的實用程序,其中元組的元素成為物件的鍵,並根據這些元素在元組中的位置提取關聯的值。這種類型在需要將元組轉換為更結構化的物件形式的情況下特別有用,允許透過元素的名稱而不是位置更直接地存取元素。

type TupleToObject<T extends [string, any][]> = {
    [P in T[number][0]]: Extract<T[number], [P, any]>[1];
};

例子

考慮這樣一個場景,您正在使用將表架構資訊儲存為元組的資料庫。每個元組包含一個欄位名稱及其對應的資料類型。這種格式通常用於資料庫元資料 API 或架構遷移工具。元組格式緊湊且易於處理,但對於應用程式開發來說,使用物件更方便。

type SchemaTuple = [
  ['id', 'number'],
  ['name', 'string'],
  ['email', 'string'],
  ['isActive', 'boolean']
];

const tableSchema: SchemaTuple = [
  ['id', 'number'],
  ['name', 'string'],
  ['email', 'string'],
  ['isActive', 'boolean'],
];

// Define the type of the transformed schema object
type TupleToObject<T extends [string, string | number | boolean][]> = {
  [P in T[number][0]]: Extract<
    T[number],
    [P, any]
  >[1];
};

type SchemaObject = TupleToObject<SchemaTuple>;

const schema: SchemaObject = tableSchema.reduce(
  (obj, [key, value]) => {
    obj[key] = value;
    return obj;
  },
  {} as SchemaObject
);

// Now you can use the schema object
console.log(schema.id);       // Output: number
console.log(schema.name);     // Output: string
console.log(schema.email);    // Output: string
console.log(schema.isActive); // Output: boolean

<a name="ExclusiveTuple"></a>

ExclusiveTuple

ExclusiveTuple類型是一個實用程序,它產生包含來自給定聯合類型T的唯一元素的元組。此類型確保聯合的每個元素僅在結果元組中包含一次,從而有效地將聯合類型轉換為具有聯合元素的所有可能的唯一排列的元組類型。這在您需要枚舉聯合成員的所有唯一組合的情況下特別有用。

type ExclusiveTuple<T, U extends any[] = []> = T extends any
    ? Exclude<T, U[number]> extends infer V
    ? [V, ...ExclusiveTuple<Exclude<T, V>, [V, ...U]>]
    : []
    : [];

例子

考慮這樣一個場景:您正在開發一個旅行應用程式的功能,該功能可以為遊覽某個城市的遊客產生獨特的行程。該市有三個主要景點:博物館、公園和劇院。

type Attraction = 'Museum' | 'Park' | 'Theater';

type Itineraries = ExclusiveTuple<Attraction>;

// The Itineraries type will be equivalent to:
// type Itineraries = 
// ['Museum', 'Park', 'Theater'] | 
// ['Museum', 'Theater', 'Park'] | 
// ['Park', 'Museum', 'Theater'] | 
// ['Park', 'Theater', 'Museum'] | 
// ['Theater', 'Museum', 'Park'] | 
// ['Theater', 'Park', 'Museum'];

<a name="PromiseType"></a>

PromiseType

PromiseType類型是一個實用程序,用於提取給定 Promise 解析為的值的類型。這在使用非同步程式碼時非常有用,因為它允許開發人員輕鬆推斷結果的類型,而無需明確指定它。

type PromiseType<T> = T extends Promise<infer U> ? U : never;

此類型使用 TypeScript 的條件類型和infer關鍵字來決定Promise的解析類型。如果T擴展Promise<U> ,則表示T是解析為類型U Promise ,而U是推斷的類型。如果T不是Promise ,則型別解析為never

例子

以下範例示範如何使用 PromiseType 類型從 Promise 中提取已解析的類型。透過使用此實用程式類型,您可以推斷 Promise 將解析為的值的類型,這有助於在處理非同步操作時進行類型檢查並避免錯誤。

type PromiseType<T> = T extends Promise<infer U> ? U : never;

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

interface Post {
  id: number;
  title: string;
  content: string;
  userId: number;
}

async function fetchUser(userId: number): Promise<User> {
  return { id: userId, name: "Anton Zamay" };
}

async function fetchPostsByUser(userId: number): Promise<Post[]> {
  return [
    {
      id: 1,
      title: "Using the Singleton Pattern in React",
      content: "Content 1",
      userId
    },
    {
      id: 2,
      title: "Hoisting of Variables, Functions, Classes, Types, " + 
             "Interfaces in JavaScript/TypeScript",
      content: "Content 2",
      userId
    },
  ];
}

async function getUserWithPosts(
  userId: number
): Promise<{ user: User; posts: Post[] }> {
  const user = await fetchUser(userId);
  const posts = await fetchPostsByUser(userId);
  return { user, posts };
}

// Using PromiseType to infer the resolved types
type UserType = PromiseType<ReturnType<typeof fetchUser>>;
type PostsType = PromiseType<ReturnType<typeof fetchPostsByUser>>;
type UserWithPostsType = PromiseType<ReturnType<typeof getUserWithPosts>>;

async function exampleUsage() {
  const userWithPosts: UserWithPostsType = await getUserWithPosts(1);

  // The following will be type-checked to ensure correctness
  const userName: UserType["name"] = userWithPosts.user.name;
  const firstPostTitle: PostsType[0]["title"] = 
    userWithPosts.posts[0].title;

  console.log(userName); // Anton Zamay
  console.log(firstPostTitle); // Using the Singleton Pattern in React
}

exampleUsage();

為什麼我們需要UserType而不僅僅是使用User

這是個好問題!使用UserType而不是直接使用User主要原因是為了確保從非同步函數的回傳類型準確推斷出類型。這種方法有幾個優點:

  1. 類型一致性:透過使用UserType ,您可以確保類型始終與fetchUser函數的實際回傳類型一致。如果fetchUser的回傳類型發生更改, UserType將自動反映該更改,而無需手動更新。

  2. 自動類型推斷:在處理複雜類型和巢狀承諾時,手動確定和追蹤解析的類型可能具有挑戰性。使用 PromiseType 允許 TypeScript 為您推斷這些類型,從而降低錯誤風險。

<a name="OmitMethods"></a>

OmitMethods

OmitMethods型別是個實用程序,可從給定型別T中刪除所有方法屬性。這意味著作為函數的類型T的任何屬性都將被省略,從而產生僅包含非函數屬性的新類型。

type OmitMethods<T> = Pick<T, { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]>;

例子

此類型在您想要從物件類型中排除方法的情況下特別有用,例如將物件序列化為 JSON 或透過 API 發送物件時,其中方法不相關且不應包含在內。以下範例示範如何將OmitMethods套用至物件類型以刪除所有方法,確保產生的類型僅包含非函數的屬性。

interface User {
  id: number;
  name: string;
  age: number;
  greet(): void;
  updateAge(newAge: number): void;
}

const user: OmitMethods<User> = {
  id: 1,
  name: "Alice",
  age: 30,
  // greet and updateAge methods are omitted from this type
};

function sendUserData(userData: OmitMethods<User>) {
  // API call to send user data
  console.log("Sending user data:", JSON.stringify(userData));
}

sendUserData(user);

<a name="FunctionArguments"></a>

FunctionArguments

FunctionArguments類型是一個實用程序,用於提取給定函數類型T的參數類型。這意味著對於傳遞給它的任何函數類型,該類型將傳回一個表示函數參數類型的元組。此類型在需要捕獲或操作函數的參數類型的情況下特別有用,例如在高階函數中或建立類型安全的事件處理程序時。

type FunctionArguments<T> = T extends (...args: infer A) => any 
  ? A 
  : never;

例子

假設您有一個高階函數包裝,它接受一個函數及其參數,然後使用這些參數來呼叫該函數。使用 FunctionArguments,您可以確保包裝函數參數的類型安全。

function wrap<T extends (...args: any[]) => any>(fn: T, ...args: FunctionArguments<T>): ReturnType<T> {
  return fn(...args);
}

function add(a: number, b: number): number {
  return a + b;
}

type AddArgs = FunctionArguments<typeof add>;
// AddArgs will be of type [number, number]

const result = wrap(add, 5, 10); // result is 15, and types are checked

<a name="Promisify"></a>

Promisify

Promisify類型是一個實用程序,它將給定類型T的所有屬性轉換為各自類型的 Promise。這意味著結果類型中的每個屬性都將是該屬性的原始類型的Promise 。這種類型在處理非同步操作時特別有用,您希望確保整個結構符合基於Promise的方法,從而更輕鬆地處理和管理非同步資料。

type Promisify<T> = {
  [P in keyof T]: Promise<T[P]>
};

例子

考慮一個顯示使用者個人資料、最近活動和設定的儀表板。這些資訊可能是從不同的服務獲取的。透過承諾單獨的屬性,我們確保使用者資料的每個部分都可以獨立取得、解析和處理,從而在處理非同步操作時提供靈活性和效率。

interface Profile {
  name: string;
  age: number;
  email: string;
}

interface Activity {
  lastLogin: Date;
  recentActions: string[];
}

interface Settings {
  theme: string;
  notifications: boolean;
}

interface UserData {
  profile: Profile;
  activity: Activity;
  settings: Settings;
}

// Promisify Utility Type
type Promisify<T> = {
  [P in keyof T]: Promise<T[P]>;
};

// Simulated Fetch Functions
const fetchProfile = (): Promise<Profile> =>
  Promise.resolve({ name: "Anton Zamay", age: 26, email: "[email protected]" });

const fetchActivity = (): Promise<Activity> =>
  Promise.resolve({
    lastLogin: new Date(),
    recentActions: ["logged in", "viewed dashboard"],
  });

const fetchSettings = (): Promise<Settings> =>
  Promise.resolve({ theme: "dark", notifications: true });

// Fetching User Data
const fetchUserData = async (): Promise<Promisify<UserData>> => {
  return {
    profile: fetchProfile(),
    activity: fetchActivity(),
    settings: fetchSettings(),
  };
};

// Using Promisified User Data
const displayUserData = async () => {
  const user = await fetchUserData();

  // Handling promises for each property (might be in different places)
  const profile = await user.profile;
  const activity = await user.activity;
  const settings = await user.settings;

  console.log(`Name: ${profile.name}`);
  console.log(`Last Login: ${activity.lastLogin}`);
  console.log(`Theme: ${settings.theme}`);
};

displayUserData();

<a name="ConstrainedFunction"></a>

ConstrainedFunction

ConstrainedFunction類型是一個實用程序,它約束給定的函數類型 T 以確保保留其參數和傳回類型。它本質上捕獲函數的參數類型和返回類型,並強制結果函數類型必須遵守這些推斷類型。當您需要對高階函數實施嚴格的類型約束或建立必須符合原始函數簽署的包裝函數時,此類型非常有用。

type ConstrainedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R
    ? (args: A extends any[] ? A : never) => R
    : never;

例子

在事先未知函數簽署且必須動態推斷的情況下, ConstrainedFunction可確保根據推斷的類型正確應用約束。想像一個實用程序,它包裝任何函數以記憶其結果:

function memoize<T extends (...args: any) => any>(fn: T): ConstrainedFunction<T> {
  const cache = new Map<string, ReturnType<T>>();
  return ((...args: Parameters<T>) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key)!;
  }) as ConstrainedFunction<T>;
}

const greet: Greet = (name, age) => {
  return `Hello, my name is ${name} and I am ${age} years old.`;
};

const memoizedGreet = memoize(greet);

const message1 = memoizedGreet("Anton Zamay", 26); // Calculates and caches
const message2 = memoizedGreet("Anton Zamay", 26); // Retrieves from cache

在這裡, memoize使用ConstrainedFunction來確保記憶函數保持與原始函數fn相同的簽名,而不需要明確定義函數類型。

<a name="UnionResolver"></a>

UnionResolver

UnionResolver類型是將聯合型別轉換為可區分聯合的實用程式。具體來說,對於給定的聯合類型T ,它會產生一個物件陣列,其中每個物件都包含一個屬性類型,該屬性類型保存聯合中的類型之一。在需要明確處理聯合的每個成員的情況下使用聯合類型時,此類型特別有用,例如在類型安全的 Redux 操作或 TypeScript 中的可區分聯合模式中。

type UnionResolver<T> = T extends infer U ? { type: U }[] : never;

例子

以下範例示範如何應用UnionResolver類型將聯合類型轉換為物件陣列,每個物件都具有type屬性。這允許對聯合內的每個操作進行類型安全處理,確保考慮到所有情況並降低使用聯合類型時發生錯誤的風險。

type ActionType = "ADD_TODO" | "REMOVE_TODO" | "UPDATE_TODO";

type ResolvedActions = UnionResolver<ActionType>;

// The resulting type will be:
// {
//   type: "ADD_TODO";
// }[] | {
//   type: "REMOVE_TODO";
// }[] | {
//   type: "UPDATE_TODO";
// }[]

const actions: ResolvedActions = [
  { type: "ADD_TODO" },
  { type: "REMOVE_TODO" },
  { type: "UPDATE_TODO" },
];

// Now you can handle each action type distinctly
actions.forEach(action => {
  switch (action.type) {
    case "ADD_TODO":
      console.log("Adding a todo");
      break;
    case "REMOVE_TODO":
      console.log("Removing a todo");
      break;
    case "UPDATE_TODO":
      console.log("Updating a todo");
      break;
  }
});

原文出處:https://dev.to/antonzo/11-20-sustom-utility-types-for-typescript-projects-2bg5


共有 0 則留言