[英]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
函數本身的返回類型也是未知的:
這是一個代碼沙盒。
在我看來,您可以采用兩種方法。
如果要強制最終函數的初始化采用特定類型,則必須事先知道該類型:
// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...
在此示例中,您在第一次調用時指定T
,因此具有以下類型約束:
tag
必須是T
的鍵transforms
必須是一個對象,其中包含T[typeof tag]
所有值的鍵source
必須是T
類型換句話說,替換T
的類型決定了tag
、 transforms
和source
可以具有的值。 這對我來說似乎是最簡單易懂的,我將嘗試為此提供一個示例實現。 但在我這樣做之前,還有方法2:
如果您希望根據tag
和transforms
的值在source
類型中具有更大的靈活性,則可以在最后一次調用時給出或推斷出類型:
const match = (tag) => (transforms) => <T>(source) => ...
在此示例中, T
在上次調用時被實例化,因此具有以下類型約束:
source
必須有一個關鍵tag
typeof source[tag]
必須是最多所有transforms
鍵的聯合,即keyof typeof transforms
。 換句話說, (typeof source[tag]) extends (keyof typeof transforms)
對於給定的source
必須始終為真。 這樣,您就不會受限於T
的特定替換,但T
最終可能是滿足上述約束的任何類型。 這種方法的一個主要缺點是幾乎沒有對transforms
進行類型檢查,因為它可以具有任何形狀。 tag
、 transforms
和source
之間的兼容性只能在最后一次調用之后檢查,這使得事情變得更加難以理解,並且任何類型檢查錯誤都可能相當神秘。 因此,我將采用下面的第一種方法(而且,這個方法很難理解;)
因為我們預先指定了類型,這將是第一個函數中的類型槽。 為了與函數的其他部分兼容,它必須擴展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.