簡體   English   中英

打字稿中模式匹配函數的返回類型

[英]return type for pattern matching function in typescript

我正在嘗試為適用於可區分聯合的打字稿創建模式匹配函數。

例如:

export type WatcherEvent =
  | { code: "START" }
  | {
      code: "BUNDLE_END";
      duration: number;
      result: "good" | "bad";
    }
  | { code: "ERROR"; error: Error };

我希望能夠輸入如下所示的match函數:

match("code")({
    START: () => ({ type: "START" } as const),
    ERROR: ({ error }) => ({ type: "ERROR", error }),
    BUNDLE_END: ({ duration, result }) => ({
      type: "UPDATE",
      duration,
      result
    })
})({ code: "ERROR", error: new Error("foo") });

到目前為止我有這個

export type NonTagType<A, K extends keyof A, Type extends string> = Omit<
  Extract<A, { [k in K]: Type }>,
  K
>;

type Matcher<Tag extends string, A extends { [k in Tag]: string }> = {
  [K in A[Tag]]: (v: NonTagType<A, Tag, K>) => unknown;
};

export const match = <Tag extends string>(tag: Tag) => <
  A extends { [k in Tag]: string }
>(
  matcher: Matcher<Tag, A>
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
) => <R extends any>(v: A): R =>
  (matcher as any)[v[tag]](v);

但我不知道如何鍵入每個案例的返回類型

目前,每種情況都正確鍵入參數,但返回類型未知,因此如果我們采用這種情況

ERROR: ({ error }) => ({ type: "ERROR", error }), // return type is not inferred presently

那么每個 case like function 的返回類型是未知的, match函數本身的返回類型也是未知的:

這是一個代碼沙盒

在我看來,您可以采用兩種方法。

1.輸入類型事先知道

如果要強制最終函數的初始化采用特定類型,則必須事先知道該類型:

// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...

在此示例中,您在第一次調用時指定T ,因此具有以下類型約束:

  1. tag必須是T的鍵
  2. transforms必須是一個對象,其中包含T[typeof tag]所有值的鍵
  3. source必須是T類型

換句話說,替換T的類型決定了tagtransformssource可以具有的值。 這對我來說似乎是最簡單易懂的,我將嘗試為此提供一個示例實現。 但在我這樣做之前,還有方法2:

2.輸入類型是從上次調用推斷的

如果您希望根據tagtransforms的值在source類型中具有更大的靈活性,則可以在最后一次調用時給出或推斷出類型:

const match = (tag) => (transforms) => <T>(source) => ...

在此示例中, T在上次調用時被實例化,因此具有以下類型約束:

  1. source必須有一個關鍵tag
  2. typeof source[tag]必須是最多所有transforms鍵的聯合,即keyof typeof transforms 換句話說, (typeof source[tag]) extends (keyof typeof transforms)對於給定的source必須始終為真。

這樣,您就不會受限於T的特定替換,但T最終可能是滿足上述約束的任何類型。 這種方法的一個主要缺點是幾乎沒有對transforms進行類型檢查,因為它可以具有任何形狀。 tagtransformssource之間的兼容性只能在最后一次調用之后檢查,這使得事情變得更加難以理解,並且任何類型檢查錯誤都可能相當神秘。 因此,我將采用下面的第一種方法(而且,這個方法很難理解;)


因為我們預先指定了類型,這將是第一個函數中的類型槽。 為了與函數的其他部分兼容,它必須擴展Record<string, any>

const match = <T extends Record<string, any>>(tag: keyof T) => ...

我們為您的示例調用它的方式是:

const result = match<WatcherEvent>('code') (...) (...)

我們將需要tag的類型來進一步構建函數,但要參數化,例如使用K會導致尷尬的 API,您必須逐字寫兩次密鑰:

 const match = <T extends Record<string, any>, K extends keyof T>(tag: K) const result = match<WatcherEvent, 'code'>('code') (...) (...)

因此,相反,我將尋求妥協,我將進一步編寫typeof tag而不是K

接下來是接受transforms的函數,讓我們使用類型參數U來保存它的類型:

const match = <T extends Record<string, any>>(tag: keyof T) => (
    <U extends ?>(transforms: U) => ...
)

U的類型約束是棘手的地方。 所以U必須是一個對象,每個T[typeof tag]值都有一個鍵,每個鍵都包含一個函數,可以將WatcherEvent轉換為您喜歡的any內容( any )。 但不僅僅是任何WatcherEvent ,特別是將相應的鍵作為code值的那個。 為了輸入這個,我們需要一個輔助類型,將WatcherEvent聯合縮小到一個成員。 概括這種行為,我想出了以下幾點:

// If T extends an object of shape { K: V }, add it to the output:
type Matching<T, K extends keyof T, V> = T extends Record<K, V> ? T : never

// So that Matching<WatcherEvent, 'code', 'ERROR'> == { code: "ERROR"; error: Error }

使用這個助手我們可以編寫第二個函數,並填寫U的類型約束如下:

const match = <T extends Record<string, any>>(tag: keyof T) => (
    <U extends { [V in T[typeof tag]]: (input: Matching<T, typeof tag, V>) => any }>(transforms: U) => ...
)

這種約束將確保在所有的函數輸入簽名transforms適合的推斷成員T工會(或WatcherEvent在你的例子)。

請注意,這里的返回類型any最終不會放松返回類型(因為我們稍后可以推斷出這一點)。 它只是意味着您可以自由地從transforms函數返回任何您想要的東西。

現在我們來到了最后一個函數——接受最終source函數,它的輸入簽名非常簡單; S必須擴展T ,其中T在您的示例中是WatcherEvent ,並且S將是給定對象的精確const形狀。 返回類型使用ReturnType標准庫的ReturnType助手來推斷匹配函數的返回類型。 實際的函數實現相當於你自己的例子:

const match = <T extends Record<string, any>>(tag: keyof T) => (
    <U extends { [V in T[typeof tag]]: (input: Matching<T, typeof tag, V>) => any }>(transforms: U) => (
        <S extends T>(source: S): ReturnType<U[S[typeof tag]]> => (
            transforms[source[tag]](source)
        )
    )
)

應該是這樣! 現在我們可以調用match (...) (...)來獲得一個函數f ,我們可以針對不同的輸入進行測試:

// Disobeying some common style rules for clarity here ;)

const f = match<WatcherEvent>("code") ({
    START       : ()                     => ({ type: "START" }),
    ERROR       : ({ error })            => ({ type: "ERROR", error }),
    BUNDLE_END  : ({ duration, result }) => ({ type: "UPDATE", duration, result }),
})

嘗試使用不同的WatcherEvent成員會得到以下結果:

const x = f({ code: 'START' })                                     // { type: string; }
const y = f({ code: 'BUNDLE_END', duration: 100, result: 'good' }) // { type: string; duration: number; result: "good" | "bad"; }
const z = f({ code: "ERROR", error: new Error("foo") })            // { type: string; error: Error; }

請注意,當您給f一個WatcherEvent (聯合類型)而不是文字值時,返回的類型也將是轉換中所有返回值的聯合,這對我來說似乎是正確的行為:

const input: WatcherEvent = { code: 'START' }
const output = f(input)

// typeof output == { type: string; }
//                | { type: string; duration: number; result: "good" | "bad"; }
//                | { type: string; error: Error; }

最后,如果您需要返回類型中的特定字符串文字而不是通用string類型,您只需更改您定義為transforms的函數即可。 例如,您可以定義額外的聯合類型,或在函數實現中使用“ as const ”注釋。

這是TSPlayground 鏈接,我希望這是您要找的!

我認為這可以實現您想要的,但是我認為您嘗試強制執行“NonTagType”是不可能的,我不確定為什么它甚至是可取的。

type Matcher<Types extends string = string, V = any> = {
  [K in Types]: (v: V) => unknown;
};

export const match = <Tag extends string>(tag: Tag) => <
  M extends Matcher
>(
  matcher: M
) => <V extends {[K in Tag]: keyof M }>(v: V) =>
  matcher[v[tag]](v) as ReturnType<M[V[Tag]]>;


const result = match("code")({
    START: () => ({ type: "START" } as const),
    ERROR: ({ error }: { error: Error }) => ({ type: "ERROR", error } as const),
    BUNDLE_END: ({ duration, result }: { duration: number, result: "good" | "bad" }) => ({
      type: "UPDATE",
      duration,
      result
    } as const)
})({ code: "ERROR", error: new Error("foo") })

這是一個非常密集的解決方案,除非我誤解了它,否則它提供了您想要的一切:

export type WatcherEvent =
 | {
      code: 'START'
      //tag: 'S'
    }
  | {
      code: 'BUNDLE_END'
      // tag: 'B'
      duration: number
      result: 'good' | 'bad'
    }
  | {
      code: 'ERROR'
      // tag: 'E';
      error: Error
    }

const match = <U extends { [K in string]: any }>(u: U) => <
  T extends keyof Extract<U, { [K in keyof U]: string }> & keyof U
>(
  tag: T
) => <H extends { [K in U[T]]: (v: Omit<Extract<U, { [P in T]: K }>, T>) => any }>(h: H) =>
  h[u[tag]](u as any) as ReturnType<H[U[T]]>

const e: WatcherEvent = {} as any // unknown event
const e2: WatcherEvent = { code: 'ERROR', error: new Error('?') } // known event

// 'code' (or 'tag' if you uncomment tag:)
const result = match(e)('code')({
  // all START, BUNDLE_END and ERROR must be specified, but nothing more
  START: v => ({ type: 'S' as const }), // v is never; v.code was removed
  BUNDLE_END: v => ({
    type: 'BE' as const,
    dur: v.duration,
    result: v.result
  }),
  ERROR: () => ({ type: 'E' } as const)
})

result.type // 'E' | 'S' | 'BE'

const r2 = match(e2)('code')({
  // since e2 is a known event START and BUNDLE_END cannot be specified
  ERROR: () => null
})
r2 === null // true

上述解決方案的最大缺點是必須提前知道目標/數據才能推斷其他類型(標簽和匹配函數)。 可以通過明確指定目標類型來改進它,如下所示:

type Matcher<U extends { [K in string]: any }> = <
  T extends keyof Extract<U, { [K in keyof U]: string }> & keyof U
>(
  tag: T
) => <
  H extends { [K in U[T]]: (v: Omit<Extract<U, { [P in T]: K }>, T>) => any }
>(
  h: H
) => (u: U) => ReturnType<H[U[T]]>;

const matcher_any = <U extends { [K in string]: any }>(t: any) => (h: any) => (e: any) => h[e[t]](e as any) as Matcher<U>;
const createMatcher = <U extends { [K in string]: any }>() =>
  matcher_any as Matcher<U>;
const matcher = createMatcher<WatcherEvent>()("code")({
  // all START, BUNDLE_END and ERROR must be specified, but nothing more
  START: (v) => ({ type: "S" as const }), // v is never; v.code was removed
  BUNDLE_END: (v) => ({
    type: "BE" as const,
    dur: v.duration,
    result: v.result
  }),
  ERROR: () => ({ type: "E" } as const)
})
console.log(matcher(e).type)

代碼沙盒

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM