我正在使用io-ts來定義我的打字稿型別,以便從單一來源進行運行時型別驗證和型別宣告。
在這個用例中,我想定義一個帶有字串成員的介面,該成員應該根據正則運算式(例如版本字串)進行驗證。所以有效的輸入看起來像這樣:
{version: '1.2.3'}
這樣做的機制似乎是通過品牌型別,我想出了這個:
import { isRight } from 'fp-ts/Either';
import { brand, Branded, string, type, TypeOf } from 'io-ts';
interface VersionBrand {
readonly Version: unique symbol; // use `unique symbol` here to ensure uniqueness across modules / packages
}
export const TypeVersion = brand(
string, // a codec representing the type to be refined
(value: string): value is Branded<string, VersionBrand> =>
/^\d \.\d \.\d $/.test(value), // a custom type guard using the build-in helper `Branded`
'Version' // the name must match the readonly field in the brand
);
export const TypeMyStruct = type({
version: TypeVersion,
});
export type Version = TypeOf<typeof TypeVersion>;
export type MyStruct = TypeOf<typeof TypeMyStruct>;
export function callFunction(data: MyStruct): boolean {
// type check
const validation = TypeMyStruct.decode(data);
return isRight(validation);
}
這在我的callFunction方法中按預期進行型別驗證,但我無法使用常規物件呼叫該函式,即以下內容無法編譯:
callFunction({ version: '1.2.3' });
這是失敗的Type 'string' is not assignable to type 'Branded<string, VersionBrand>'。
雖然該錯誤資訊是有道理的,因為Version是的一個特例string,我想允許呼叫者呼叫任何字串的函式,然后做一個運行時確認,將檢查對正則運算式。我仍然想要一些輸入資訊,所以我不想將輸入定義為any.
理想情況下,有一種方法可以從中派生出一個版本VersionForInput,Version該版本使用標記欄位的原始資料型別,因此它等效于:
interface VersionForInput { version: string }
當然,我可以明確宣告它,但這意味著將型別定義復制到某種程度。
問題: io-ts 有沒有辦法從型別的品牌版本中派生出非品牌版本?使用品牌型別甚至是此用例的正確選擇嗎?目標是在原始型別之上進行額外的驗證(例如,額外的正則運算式檢查字串值)。
uj5u.com熱心網友回復:
我想你已經問了兩個問題,所以我會盡量回答這兩個問題。
從品牌型別中提取基本型別
有可能在其上反思io-ts編解碼器正在通過品牌type欄位上的一個實體Brand。
TypeVersion.type // StringType
因此,可以撰寫一個使用您的結構型別并從基礎創建編解碼器的函式。您還可以使用以下內容提取型別:
import * as t from 'io-ts';
type BrandedBase<T> = T extends t.Branded<infer U, unknown> ? U : T;
type Input = BrandedBase<Version>; // string
使用品牌型別
但我認為不是以這種方式定義事物,而是定義我的結構輸入型別,然后定義一個細化,這樣您只需指定從輸入中細化的編解碼器部分。這將使用較新的io-tsapi。
import { pipe } from 'fp-ts/function';
import * as D from 'io-ts/Decoder';
import * as t from 'io-ts';
import * as E from 'fp-ts/Either';
const MyInputStruct = D.struct({
version: D.string,
});
interface VersionBrand {
readonly Version: unique symbol; // use `unique symbol` here to ensure uniqueness across modules / packages
}
const isVersionString = (value: string): value is t.Branded<string, VersionBrand> => /^\d \.\d \.\d $/.test(value);
const VersionType = pipe(
D.string,
D.refine(isVersionString, 'Version'),
);
const MyStruct = pipe(
MyInputStruct,
D.parse(({ version }) => pipe(
VersionType.decode(version),
E.map(ver => ({
version: ver,
})),
)),
);
這將首先定義輸入型別,然后將輸入型別的細化定義為更嚴格的內部解碼器。如果你有其他價值觀,你可以...rest通過它們E.map來避免重復自己。
要回答“我是否應該使用品牌型別”這個問題,我認為您應該,但我想提供關于如何使用io-ts驗證的觀點的變化。有一篇關于在應用程式邊緣使用決議邏輯的有趣文章,以便您可以決議一次并依賴應用程式核心內的更強型別。
因此,我建議您Version盡早決議字串,然后在應用程式的其他任何地方討論品牌型別。因此,例如,如果版本作為命令列引數出現,您可能有以下內容:
import { pipe } from 'fp-ts/function';
import * as t from 'io-ts';
import * as E from 'fp-ts/Either';
// Version being the branded type
function useVersion(version: Version) {
// Does something with the branded type
}
function main (args: unknown) {
pipe(
MyStructType.decode(args) // accepts unknown
// Map is called on Right only and passes Lefts along so you can
// focus in on specifically what to do if validation succeeded
E.map(({ version }) => useVersion(version)),
// later on maybe handle Left
E.fold(
(l: t.Errors): void => console.error(l),
(r) => {},
),
),
}
uj5u.com熱心網友回復:
對我的用例來說很好的是定義一個沒有品牌的輸入型別,并將細化定義為該型別與品牌的交集,例如:
export const TypeMyStructIn = type({
version: string,
});
export const TypeMyStruct = intersection([
TypeMyStructIn,
type({
version: TypeVersion,
}),
]);
export type Version = TypeOf<typeof TypeVersion>;
export type MyStruct = TypeOf<typeof TypeMyStruct>;
export type MyStructIn = TypeOf<typeof TypeMyStructIn>;
export function callFunction(data: MyStructIn): boolean {
// type check
const validation = TypeMyStruct.decode(data);
return isRight(validation);
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/312695.html
