Skip to main content

[note] Yup 筆記

TL;DR

  • 不論是 yup.mixed()yup.string()yup.number() 或是 yup.boolean() 等,它們在執行後都會產生一個 schemayup.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:該欄位的值是,預設會用 === 比較,也可以帶入 function
  • then:若 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() 搭配 isthenotherwise 可以讓我們根據表單中的某「一個」欄位來決定特地欄位的驗證規則,但這只適用在根據「單一個」欄位時。如果某個欄位的驗證規則是需要根據多個欄位來決定的話,則可以使用 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 的值:

  • kindamount 時,value 必須是 number
  • kindoptions 是,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() 這個方法!

tip

在使用 mixed.when()mixed.test() 中的 callback function 時,盡可能用 try...catch 包起來,才不會發生了錯誤仍不自知。