开发TypeScript库时是否需要手动校验参数?
但凡对TypeScript的工作原理有粗浅了解,都能回答:‘需要。TypeScript仅会将代码转译为JavaScript,不会在运行时添加包括类型检查在内的额外行为。’
例如:
function example(parameter: string): void { console.log(parameter.charAt(1)); }
会被转译为:
"use strict"; function example(parameter) { console.log(parameter.charAt(1)); }
若JavaScript用户调用example(3),会抛出错误Uncaught TypeError: parameter.charAt is not a function。
为何纠结这个问题?
因为我计划花费数十小时优化我的库@yamato-daiwa/es-extensions,但更关键的是,这会让库体积数倍增长,绝非夸大其词。
无校验的addElementsToArray函数(1.6.x版本)
export default function addElementsToArray<ArrayElement>( namedParameters: { targetArray: Array<ArrayElement>; newElements: Array<ArrayElement>; mutably: boolean; } & ( { toStart: true; } | { toEnd: true; } | { toPosition__numerationFrom0: number; } | { toPosition__numerationFrom1: number; } ) ): Array<ArrayElement> { const workpiece: Array<ArrayElement> = namedParameters.mutably ? namedParameters.targetArray : [ ...namedParameters.targetArray ]; if ("toStart" in namedParameters) { workpiece.unshift(...namedParameters.newElements); return workpiece; } if ("toEnd" in namedParameters) { workpiece.push(...namedParameters.newElements); return workpiece; } const positionOfFirstNewElement__numerationFrom0: number = "toPosition__numerationFrom0" in namedParameters ? namedParameters.toPosition__numerationFrom0 : namedParameters.toPosition__numerationFrom1 - 1; workpiece.splice(positionOfFirstNewElement__numerationFrom0, 0, ...namedParameters.newElements); return workpiece; }
添加校验后的addElementsToArray函数
import Logger from "../Logging/Logger"; import InvalidParameterValueError from "../Errors/InvalidParameterValue/InvalidParameterValueError"; import IncompatiblePropertiesInObjectTypeParameterError from "../Errors/IncompatiblePropertiesInObjectTypeParameter/IncompatiblePropertiesInObjectTypeParameterError"; import stringifyAndFormatArbitraryValue from "../Strings/stringifyAndFormatArbitraryValue"; import isArbitraryObject from "../TypeGuards/Objects/isArbitraryObject"; import isNotUndefined from "../TypeGuards/Nullables/isNotUndefined"; import isNonNegativeInteger from "../TypeGuards/Numbers/isNonNegativeInteger"; import isNaturalNumber from "../TypeGuards/Numbers/isNaturalNumber"; export default function addElementsToArray<ArrayElement>( namedParameters: Readonly< ( { mutably: true; targetArray: Array<ArrayElement>; } | { mutably: false; targetArray: ReadonlyArray<ArrayElement>; } ) & { newElements: ReadonlyArray<ArrayElement>; toStart?: true; toEnd?: true; toPosition__numerationFrom0?: number; toPosition__numerationFrom1?: number; } > ): Array<ArrayElement> { if (!isArbitraryObject(namedParameters)) { Logger.throwErrorAndLog({ errorInstance: new InvalidParameterValueError({ parameterNumber: 1, parameterName: "namedParameters", messageSpecificPart: "The first and only parameter of 'addElementsToArray' must be of the object type." }), title: InvalidParameterValueError.localization.defaultTitle, occurrenceLocation: "addElementsToArray(namedParameters)" }); } if (!Array.isArray(namedParameters.targetArray)) { Logger.throwErrorAndLog({ errorInstance: new InvalidParameterValueError({ parameterName: "namedParameters", parameterNumber: 1, messageSpecificPart: "The 'targetArray' is not the valid array and has value:\n" + `${ stringifyAndFormatArbitraryValue(namedParameters.targetArray) }` }), title: InvalidParameterValueError.localization.defaultTitle, occurrenceLocation: "addElementsToArray(namedParameters)" }); } const alternativelyRequestOptions: Array<true | number> = [ namedParameters.toStart, namedParameters.toEnd, namedParameters.toPosition__numerationFrom0, namedParameters.toPosition__numerationFrom1 ].filter(isNotUndefined); if (alternativelyRequestOptions.length > 1) { Logger.logError({ errorType: IncompatiblePropertiesInObjectTypeParameterError.NAME, title: IncompatiblePropertiesInObjectTypeParameterError.localization.defaultTitle, description: "Exactly one of 'toStart', 'toEnd', 'toPosition__numerationFrom0', 'toPosition__numerationFrom1' " + "must be specified while actually multiple of them has been. No elements will be added to target array.", occurrenceLocation: "addElementsToArray(namedParameters)" }); return namedParameters.targetArray; } const workpiece: Array<ArrayElement> = namedParameters.mutably ? namedParameters.targetArray : [ ...namedParameters.targetArray ]; if (namedParameters.toStart === true) { workpiece.unshift(...namedParameters.newElements); return workpiece; } if (namedParameters.toEnd === true) { workpiece.push(...namedParameters.newElements); return workpiece; } let positionOfFirstNewElement__numerationFrom0: number; if (isNonNegativeInteger(namedParameters.toPosition__numerationFrom0)) { positionOfFirstNewElement__numerationFrom0 = namedParameters.toPosition__numerationFrom0; } else if (isNaturalNumber(namedParameters.toPosition__numerationFrom1)) { positionOfFirstNewElement__numerationFrom0 = namedParameters.toPosition__numerationFrom1 - 1; } else { Logger.logError({ errorType: InvalidParameterValueError.NAME, title: InvalidParameterValueError.localization.defaultTitle, description: alternativelyRequestOptions.length === 0 ? "None one of 'toStart', 'toEnd', 'toPosition__numerationFrom0', 'toPosition__numerationFrom1' has been specified." : ( "The specified value of 'toStart', 'toEnd', 'toPosition__numerationFrom0' or 'toPosition__numerationFrom1' has " + "invalid type, value or numbers set." ) + "No elements will be added to target array.", occurrenceLocation: "addElementsToArray(namedParameters)" }); return namedParameters.targetArray; } workpiece.splice(positionOfFirstNewElement__numerationFrom0, 0, ...namedParameters.newElements); return workpiece; }
此时校验代码约占一半行数,还引入了大量依赖。有时校验代码甚至会占函数行数的90%以上,库的分发包体积会数倍增长,这对每KB都至关重要的前端应用来说是个关键问题。
上述校验还不够细致!我们还可以:
- 当互斥的命名参数(
toStart、toEnd、toPosition__numerationFrom0、toPosition__numerationFrom1)传入多个时,检查具体是哪些参数并记录名称。 - 将这些参数的类型检查与未传入任何参数的检查分开(目前这两项检查是同时进行的)。
- 若上述参数的子类型不正确(例如
toStart或toEnd为false,或toPosition__numerationFrom0/toPosition__numerationFrom1为负数或小数),单独记录该问题。
潜在的虚假问题
简而言之,非TypeScript用户可能会因参数类型无效而向我报告Bug,但如果用TypeScript编写相同代码,TypeScript会提前抛出错误。即便最终发现问题是因使用JavaScript而非TypeScript导致的参数类型无效,也会浪费大量调查和沟通时间。这类问题可能数量众多,但本质上只是TypeScript使用不当。
悬赏更新说明
这个问题已有答案,但参数校验可能导致库体积数倍增长的问题仍困扰着我。为彻底解决该问题,我需要资深/大师级Web开发者给出明确建议:要么“你做得对,继续坚持!即便因参数校验导致分发包体积数倍增长,对前端应用也无关紧要。”,要么“即便因参数校验,过大的工具库体积也不可取。你应跳过参数校验以保持库的轻量化。”
内容的提问来源于stack exchange,提问作者Takeshi Tokugawa YD

