[note] Yup 筆記
- Yup @ Github
- Yup Sandbox @ pjchender runkit
TL;DR
- 不論是
yup.mixed()
、yup.string()
、yup.number()
或是yup.boolean()
等,它們在執行後都會產生一個 schema。yup.mixed()
產生的 schema 會帶有所有型別都可以使用的方法,而其他如 StringSchema、NumberSchema、等等則是會多了一些屬於該型別可以使用的方法。 - 因為 yup 已經使用了 JS 內建的錯誤處理在顯示型別錯誤,因此 yup 參數中使用的 callback function 發生程式錯誤時,這個錯誤並不會被拋出,看起來會「沒有發生任何事」,但實際上驗證功能已經故障,這特別容易發生在
mixed.when()
或mixed.test()
的使用,因此建議在 yup 中使用的 callback function 最後都有try...catch
包起來。
mixed
yup.mixed()
的意思是建立一個能夠匹配到所有型別的 schema,其他所有型別所產生的 schema 也都是繼承自這個 schema。
mixed.validate:檢驗 value 是否符合 schema
mixed.validate(value: any, options?: object): Promise<any, ValidationError>
可以用來檢驗 value
和 schema 是否相符合:
import {
number as yupNumber,
object as yupObject,
string as yupString,
ValidationError,
} from 'yup';
const schema = yupObject().shape({
name: yupString().required(),
age: yupNumber().min(18).required(),
});
schema
.validate({
name: 'Aaron',
age: 'foo',
})
.then((value) => console.log({ value }))
.catch((err: ValidationError) => {
const { errors, name } = err;
console.log({
name,
errors,
});
});
mixed.cast:根據 schema 把 value 作轉換後輸出
mixed.cast(value: any, options = {}): any
透過 cast
只會將資料做轉換 ,例如把字串 '10'
變成數值 10
,不會做驗證(validate)的動作:
const schema = yupObject().shape({
name: yupString().required(),
age: yupNumber().min(18).required(),
});
const value = schema.cast({
name: 'Aaron',
age: '10',
});
// { age: 10, name: 'Aaron' }
mixed.default:用來將 schema 中的欄位設定預設值
mixed.default(value: any): Schema
const schema = yupObject().shape({
name: yupString().required().default('Aaron'),
age: yupNumber().min(18).required(),
});
const defaultValue = schema.getDefault(); // { age: undefined, name: 'Aaron' }
const value = schema.cast({ age: '10' }); // { age: 10, name: 'Aaron' }
mixed.when:根據 schema 中的其他欄位來改變驗證的規則
mixed.when(keys: string | Array<string>, builder: object | (value, schema) => Schema): Schema
在 builder
中可以直接帶入物件,其中包含屬性
is
:該欄位的值是,預設會用===
比較,也可以帶入 functionthen
:若is
的條件滿足true
,則會使用這裡的規則otherwise
:若is
的條件為false
,則會使用這裡的規則
const schema = yupObject().shape({
isBig: yupBool(),
count: yupNumber().when('isBig', {
is: true, // 當 isBig 的值是 true
then: yupNumber().min(10), // 當 is 成立時,則使用 then 的規則(count >= 10)
otherwise: yupNumber().max(5), // 當 is 不成立時,則使用 otherwise 的規則(count <= 5)
}),
});
schema
.validate({
isBig: false, // 根據 isBig 的值來決定 count 驗證的邏輯
count: 7,
})
.then((value) => value)
.catch((err) => err);
when
的第二個參數也可以放函式:
const schema = yupObject().shape({
isBig: yupBool(),
count: yupNumber().when('isBig', (value: boolean, schema: NumberSchema) =>
// 如果 isBig 為 true,則驗證 count 的值是否 >= 10,否則驗證是否 <= 5
value ? schema.min(10) : schema.max(5),
),
});
schema.validate({
isBig: true, // 根據 isBig 的值來決定 count 驗證的邏輯
count: 9,
});
也可以搭配 context
使用,或同時串接多個 when
使用:
const schema = yupObject().shape({
isBig: yupBool(),
count: yupNumber()
.when('isBig', (value: boolean, schema: NumberSchema) =>
// isBig 為真,count 需 >= 10;否則 count <= 5
value ? schema.min(10) : schema.max(5),
)
.when('$type', (type: string, schema: NumberSchema) =>
// $type 等於 "NARROW" 則 count 需 <= 3
type === 'NARROW' ? schema.max(3) : schema,
),
});
schema
.validate(
{
isBig: false,
count: 4,
},
// context 中定義 type 這個變數
{ context: { type: 'NARROW' } },
)
.then((value) => value)
.catch((err) => err);
也可以根據多個變數來決定要使用的規則:
const schema = yupObject().shape({
isBig: yupBool(),
// 同時根據 isBig 和 isNarrow 來決定要用哪個驗證邏輯
count: yupNumber().when(['isBig', 'isNarrow'], {
is: (isBig: boolean, isNarrow: boolean) => !isBig && isNarrow,
then: yupNumber().min(2),
otherwise: yupNumber().max(10),
}),
});
schema
.validate({
isBig: false,
isNarrow: true,
count: 4,
})
.then((value) => value)
.catch((err) => err);
mixed.test:用來建立自訂的驗證邏輯
keywords: validation schema by multiple fields
mixed.test(name: string, message: string | function, test: function): Schema
除了 Yup 針對個型別內建的規則之外(例如,min()
, oneOf()
)透過 test
可以建立客製化的驗證規則,其中 test
必須包含
name: string
message: string | function
:驗證錯誤時顯示的訊息,在message
中可以透過${}
的方式,把testContext.params
中的參數自動替換進去。test: function
:用來驗證的規則,回傳true
(valid)、false
(invalid)
const customSchema = yupString().test(
'is-aaron', // name
'${path} is not Aaron', // message
(value, testContext) => {
if (value !== 'Aaron') {
// 產生錯誤訊息
return testContext.createError({
message: '${originalValue} is not Aaron',
});
}
return value === 'Aaron';
},
);
// valid
customSchema
.validate('Aaron')
.then((value) => console.log(value))
.catch((err) => console.log(err));
// invalid
customSchema
.validate('aaron')
.then((value) => console.log(value))
.catch((err: ValidationError) =>
console.log({
name: err.name,
message: err.message,
errors: err.errors,
}),
);
// {
// name: 'ValidationError',
// message: 'aaron is not Aaron',
// errors: [ 'aaron is not Aaron' ]
// }
test
是可以串連起來使用的,例如:
const validationSchema = yup.object().shape({
effectiveDate: yup
.string()
.required('effective date required')
.test('invalid date', 'effective date is invalid', startDate => isDateStringValid(startDate, DATE_FORMAT.SLASH))
.test('earlier than today', 'effective date is earlier than today', startDate => isSameOrAfterDate(startDate)),
});
此外,test
除了可以透過帶入多個參數的方式,也可以透過帶入物件的方式來使用:
const customSchema = yupString().test({
name: 'is-aaron',
test: (value) => value === 'Aaron',
params: { expect: 'Aaron' }, // 這裡的值會可以被帶到 message 中使用
message: '${value} is not ${expect}',
});
customSchema
.validate('aaron')
.then((value) => console.log(value))
.catch((err: ValidationError) => console.log(err));
string, number, boolean, date
在 Yup 中提供像是 yup.string()
、yup.number()
、yup.boolean()
等等。
和 yup.mixed()
一樣,執行之後它會產生一個 schema,但這些 schema 除了可以使用上述 yup.mixed()
所提供的方法,還多了屬於該型別 schema 的方法可以使用。
常用情境
使用 yup 的 test 來根據其他欄位來驗證特定欄位
透過 Yup 提供的 mixed.when()
搭配 is
、then
和 otherwise
可以讓我們根據表單中的某「一個」欄位來決定特地欄位的驗證規則,但這只適用在根據「單一個」欄位時。如果某個欄位的驗證規則是需 要根據多個欄位來決定的話,則可以使用 Yup 提供的 mixed.test()
方法。
使用 mixed.test()
方法時,可以透過最後一個參數的 callback function 中取得其他欄位的值:
let jimmySchema = string().test(
'is-jimmy', // name of the validation schema
'${path} is not Jimmy', // error msg
(value, context) => value === 'jimmy', // callback function
);
在最後的這個 callback function 中,透過 context.parent
就可以取得其他欄位當前的值,當需要根據 多個欄位來決定驗證規則時非常實用。
某一個欄位有兩種以上的不同驗證邏輯(例如不同型別)
在下面的例子中,value
有可能是 string 或 number,它的型別會取決於 kind
的值:
- 當
kind
是amount
時,value
必須是number
- 當
kind
是options
是,value
則需要是string
寫法一: mixed.when() 中使用 yup
其中一種寫法是在 when()
裡面使用 yup.string()
或 yup.number()
:
const yup = require('yup');
const validationSchema = yup.object({
kind: yup.string().typeError('Must be string').required('Must be existed'),
value: yup.mixed().when('kind', (kind, schema) => {
try {
// 其中一種寫法是在 `when()` 裡面使用 `yup.string()` 或 `yup.number()`:
switch (kind) {
case 'amount':
return yup
.number()
.typeError('should be number') // type error message
.required('should not be empty'); // required field error message
case 'options':
return yup
.string()
.typeError('should be string') // type error message
.required('should not be empty'); // required field error message
default:
// do nothing
return schema;
}
} catch (error) {
console.log(error);
}
}),
});
const t1 = {
kind: 'amount',
value: 10,
};
const t2 = {
kind: 'options',
value: 'react',
};
validationSchema
.validate(t2, { strict: true })
.then((value) => console.log({ value }))
.catch((err) => console.log({ name: err.name, errors: err.errors }));
方法二:mixed.when 搭配 mixed.test
另一種方式是在 mixed.when()
中搭配使用 mixed.test()
,寫法會類似這樣:
yup.mixed()
的 schema 能夠使用 mixed 的所有方法yup.string()
的 stringSchema 則能夠使用 mixed 所有的方法和 stringSchema 額外提供的方法
const yup = require('yup');
const validationSchema = yup.object({
kind: yup.string().typeError('Must be string').required('Must be existed'),
value: yup.mixed().when('kind', (kind, schema) => {
try {
switch (kind) {
case 'amount':
return schema.test(
'is-number', // name
'amount should be number', // error message
(value, testContext) => typeof value === 'number', // test function
);
case 'options':
return schema.test(
'is-string', // name
'options should be string', // error message
(value, testContext) => typeof value === 'string', // test function
);
default:
return schema;
}
} catch (error) {
console.log(error);
}
}),
});
const t1 = {
kind: 'amount',
value: 10,
};
const t2 = {
kind: 'options',
value: 'React',
};
validationSchema
.validate(t1, { strict: true })
.then((value) => console.log({ value }))
.catch((err) => console.log({ name: err.name, errors: err.errors }));
留意
Yup 內的錯誤常常無聲無息
在使用 Yup 時要特別留意,因為原本的錯誤處理已經被用來當作 schema validation 的錯誤訊息使用,因此當我在 Yup 內寫出錯誤的程式碼時,它並不會向外拋出錯誤,這個錯誤會「無聲無息但 validation 已經無法正常作用」,因此非常建議使用 try...catch
幫助留意使用 yup 時可能發生的錯誤,這樣的問題特別容易發生在使用 yup.when()
或 yup.test()
這種會透過 callback 來客製化驗證規則的情況。
舉例來說,當我們這樣使用 mixed().when()
時,並不會看到任何錯誤產生:
const yup = require('yup');
const user = {
age: 30,
isAdult: true,
};
const userValidation = yup.object({
age: yup.number().required(),
isAdult: yup.number().when('age', (age, schema) => {
// 這裡面已經發生錯誤
if (age > 18) {
return schema.boolean();
}
return schema;
}),
});
userValidation
.validate(user, { strict: true })
.then((value) => console.log({ value }))
.catch((err) => console.log({ name: err.name, errors: err.errors }));
但實際上,使用 try...catch
後我們會發現在 .when()
裡已經發生錯誤:
const userValidation = yup.object({
age: yup.number().required(),
isAdult: yup.number().when('age', (age, schema) => {
// 使用 try...catch
try {
if (age > 18) {
return schema.boolean();
}
return schema;
} catch (error) {
console.log('error', error);
}
}),
});
將可以看到 "schema.boolean is not a function" 的錯誤訊息,之所以會有這個錯誤是因為 schema 並沒有 .boolean()
這個方法!
在使用 mixed.when()
或 mixed.test()
中的 callback function 時,盡可能用 try...catch
包起來,才不會發生了錯誤仍不自知。