TypeScript中map函数需返回类型注解的原因及规避方案
我希望映射一个元素列表并修改每个元素的一个属性,但如果属性名拼写错误,TypeScript可能无法检测到。
定义一个类型(Dog):
type Dog = { breed: string; name: string; };
创建一个狗狗列表:
const dogs: Dog[] = [];
现在映射该狗狗列表,修改每只狗的breed属性,但我在赋值breed字段时出现拼写错误,TypeScript却没有提示。这会导致两个问题:
- A) 未能真正修改目标属性;
- B)
newDogs1的实际类型并非Dog[],但TypeScript却认为它是。
const newDogs1: Dog[] = dogs.map((dog) => { return { ...dog, breeeeeeeeeeeed: 'typo!', // no error :( }; });
如果按如下方式编写代码,TypeScript会给出警告,但这种写法不够自然:
const newDogs2: Dog[] = dogs.map((dog) => { const result: Dog = { ...dog, breeeeeeeeeeeed: 'typo!', // error :) }; return result; });
为map函数添加类型参数无法解决该问题。为何如此?传入map的函数类型为(value: Dog, index: number, array: Dog[]) => Dog,其返回类型为Dog,按理说该函数必须返回Dog类型的对象。而当前返回的是Dog & {/*something*/}类型的对象,它可赋值给Dog但并非Dog类型。但newDogs2中将该类型对象赋值给Dog时会报错,那为何newDogs3没有同样的错误?
const newDogs3: Dog[] = dogs.map<Dog>((dog) => { return { ...dog, breeeeeeeeeeeed: 'typo!', // no error :( }; });
为map的回调函数添加返回类型注解后,TypeScript就能识别问题:
const newDogs4: Dog[] = dogs.map((dog): Dog => { return { ...dog, breeeeeeeeeeeed: 'typo!', // error :) }; });
newDogs3和newDogs4中map函数的类型是相同的:Array<Dog>.map<Dog>(callbackfn: (value: Dog, index: number, array: Dog[]) => Dog, thisArg?: any): Dog[]
- 为何给
map传入<Dog>类型参数(见newDogs3)无错误,而给map的回调函数添加: Dog返回类型注解(见newDogs4)会报错? - 如何避免该陷阱?
newDogs1的写法十分常见,是否有TypeScript配置或ESLint规则可协助规避?
问题1解答
这是因为TypeScript在两种场景下的类型检查逻辑不同:
- 给
map指定<Dog>泛型参数时,TypeScript做的是赋值兼容性检查:返回的对象包含Dog的所有必填属性(通过展开dog获得),即使多了拼写错误的属性,它依然是Dog的超类型,结构类型系统允许超类型赋值给子类型,因此不会报错,多余的属性会被TypeScript忽略。 - 给回调函数显式添加
: Dog返回类型注解时,TypeScript触发的是严格类型匹配检查:要求返回值必须完全符合Dog的定义,不允许存在任何未定义的额外属性,因此拼写错误的属性会被检测出来并报错。
问题2解答
有几种实用的规避方案:
1. 启用TypeScript严格模式
在tsconfig.json中设置"strict": true,它包含的strictNullChecks等规则会强化类型检查的严谨性,配合TypeScript原生的对象字面量额外属性检查(仅在字面量直接赋值给类型变量时触发),能在部分场景下提前发现问题。
2. 用ESLint规则强制添加函数返回类型
启用@typescript-eslint/explicit-function-return-type规则,强制所有函数(包括map的回调)添加返回类型注解。这样写newDogs1这类代码时,会被要求补充: Dog返回类型,从而触发严格检查捕获拼写错误。
3. 用工具函数约束返回值
编写简单的工具函数来强制返回值符合目标类型:
const assertDog = (value: Dog): Dog => value; const newDogs = dogs.map(dog => assertDog({ ...dog, breeeeeeeeeeeed: 'typo!', // 此处会报错 }));
4. 明确属性修改的类型写法
如果是修改已知属性,直接用Omit或明确的类型合并写法,借助IDE的自动补全避免拼写错误:
const newDogs = dogs.map(dog => ({ ...dog, breed: 'Golden Retriever', // IDE会自动提示正确属性名 }));
内容的提问来源于stack exchange,提问作者nullromo

