77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
/**
|
||
* shared 层内部工具,不对外导出。
|
||
*
|
||
* HTTP / WebSocket 收到的一切都先当 unknown,再用这些收窄函数校验;
|
||
* 直接断言会把服务端字段改名这类问题推迟到运行期某个深处才炸。
|
||
*/
|
||
|
||
/**
|
||
* FormData / Blob(含 File)/ URLSearchParams 不是普通对象:
|
||
* 展开或 JSON 序列化都会丢掉内容,必须原样交给 fetch(报名表导入就是被展平才丢文件)。
|
||
*/
|
||
export function isRawBody(value: unknown): value is FormData | Blob | URLSearchParams {
|
||
if (typeof FormData !== 'undefined' && value instanceof FormData) return true
|
||
if (typeof Blob !== 'undefined' && value instanceof Blob) return true
|
||
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) return true
|
||
return false
|
||
}
|
||
|
||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||
return typeof value === 'object' && value !== null && !Array.isArray(value) && !isRawBody(value)
|
||
}
|
||
|
||
export function readString(source: unknown, key: string): string | null {
|
||
if (!isRecord(source)) return null
|
||
const value = source[key]
|
||
return typeof value === 'string' ? value : null
|
||
}
|
||
|
||
export function asRecord(value: unknown): Record<string, unknown> {
|
||
return isRecord(value) ? value : {}
|
||
}
|
||
|
||
/** 依次取第一个可用值:容忍服务端把数字写成字符串(settings 表存的是文本)。 */
|
||
export function firstString(...values: unknown[]): string | null {
|
||
for (const value of values) {
|
||
if (typeof value === 'string') return value
|
||
}
|
||
return null
|
||
}
|
||
|
||
export function firstNumber(def: number, ...values: unknown[]): number {
|
||
const found = firstNumberOrNull(...values)
|
||
return found === null ? def : found
|
||
}
|
||
|
||
export function firstNumberOrNull(...values: unknown[]): number | null {
|
||
for (const value of values) {
|
||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||
if (typeof value === 'string' && value.trim() !== '') {
|
||
const parsed = Number(value)
|
||
if (Number.isFinite(parsed)) return parsed
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
export function firstBool(def: boolean, ...values: unknown[]): boolean {
|
||
for (const value of values) {
|
||
if (typeof value === 'boolean') return value
|
||
if (typeof value === 'number') return value !== 0
|
||
if (typeof value === 'string') {
|
||
const normalized = value.trim().toLowerCase()
|
||
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') {
|
||
return true
|
||
}
|
||
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
export function emptyToNull(value: string | null): string | null {
|
||
return value === null || value.trim() === '' ? null : value
|
||
}
|