在将一些代码迁移到TypeScript时,我遇到了一些小障碍,想和大家分享一下。
用例
在EpicReact.dev研讨会上,当我教授如何进行HTTP请求时,我使用GraphQL Pokemon API。这里是我们如何提出请求的:
const formatDate = date =>
`${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')} ${String(
date.getSeconds(),
).padStart(2, '0')}.${String(date.getMilliseconds()).padStart(3, '0')}`
async function fetchPokemon(name) {
const pokemonQuery = `
query PokemonInfo($name: String) {
pokemon(name: $name) {
id
number
name
image
attacks {
special {
name
type
damage
}
}
}
}
`
const response = await window.fetch('https://graphql-pokemon2.vercel.app/', {
// learn more about this API here: https://graphql-pokemon2.vercel.app/
method: 'POST',
headers: {
'content-type': 'application/json;charset=UTF-8',
},
body: JSON.stringify({
query: pokemonQuery,
variables: {name: name.toLowerCase()},
}),
})
const {data, errors} = await response.json()
if (response.ok) {
const pokemon = data?.pokemon
if (pokemon) {
// add fetchedAt helper (used in the UI to help differentiate requests)
pokemon.fetchedAt = formatDate(new Date())
return pokemon
} else {
return Promise.reject(new Error(`No pokemon with the name "${name}"`))
}
} else {
// handle the graphql errors
const error = new Error(errors?.map(e => e.message).join('\n') ?? 'unknown')
return Promise.reject(error)
}
}
下面是一个使用/输出的例子:
fetchPokemon('pikachu').then(data => console.log(data))
这个日志:
{
"id": "UG9rZW1vbjowMjU=",
"number": "025",
"name": "Pikachu",
"image": "https://img.pokemondb.net/artwork/pikachu.jpg",
"attacks": {
"special": [
{
"name": "Discharge",
"type": "Electric",
"damage": 35
},
{
"name": "Thunder",
"type": "Electric",
"damage": 100
},
{
"name": "Thunderbolt",
"type": "Electric",
"damage": 55
}
]
},
"fetchedAt": "16:18 39.159"
}
而对于错误的情况:
fetchPokemon('not-a-pokemon').catch(error => console.error(error))
// Logs: No pokemon with the name "not-a-pokemon"
如果我们犯了一个GraphQL错误(例如,打字image 为imag ),那么我们会得到:
{
"message": "Cannot query field \"imag\" on type \"Pokemon\". Did you mean \"image\"?"
}
输入fetch
好了,现在我们知道fetchPokemon 应该做什么了,让我们开始添加类型。
下面是我如何将代码迁移到TypeScript:
- 更新文件名为
.ts(如果项目使用React,则为.tsx),以便在文件中启用TypeScript - 更新所有在我的编辑器中出现红色小方块的代码,直到它们消失。通常,我从导出的函数的输入开始。
在这种情况下,一旦我们在这个文件上启用TypeScript,我们就会得到其中的三个:
Parameter 'such-and-such' implicitly has an 'any' type. ts(7006)
就这样了。每个函数都有一个。所以从一开始,这似乎就会是一个小事,对吗?
所以我们修复了所有的这些:
const formatDate = (date: Date) => {
// ...
}
async function fetchPokemon(name: string) {
// ...
if (response.ok) {
// ...
} else {
// NOTE: Having to explicitly type the argument to `.map` means that
// the array you're maping over isn't typed properly! We'll fix this later...
const error = new Error(
errors?.map((e: {message: string}) => e.message).join('\n') ?? 'unknown',
)
// ...
}
}
现在这些错误都消失了!
使用打出的fetchPokemon
很好,所以让我们使用这个东西:
async function pikachuIChooseYou() {
const pikachu = await fetchPokemon('pikachu')
console.log(pikachu.attacks.special.name)
}
我们运行这个,然后......呃哦......。你发现了吗?我们给自己找了一个类型错误 😱special 是一个数组!所以应该是pikachu.attacks.special[0].name 。fetchPokemon 的返回值是Promise<any> 。看来,我们还没有完全完成。所以,让我们输入预期的PokemonData 返回值:
type PokemonData = {
id: string
number: string
name: string
image: string
fetchedAt: string
attacks: {
special: Array<{
name: string
type: string
damage: number
}>
}
}
很好,有了这个,现在我们可以对我们的返回值进行更明确的说明:
async function fetchPokemon(name: string): Promise<PokemonData> {
// ...
}
而现在我们会得到一个类型错误,就是我们之前的那个用法,我们可以纠正它。
删除any东西
好了,让我们来看看errors.map 调用的那个不幸的明确类型。正如我前面提到的,这是一个迹象,表明我们的数组没有正确的类型。
快速回顾一下就会发现,data 和errors 都是any:
const {data, errors} = await response.json()
这是因为response.json 的返回类型是Promise<any> 。当我第一次意识到这一点时,我很恼火,但在思考了一秒钟后,我意识到我不知道它还能是什么!TypeScript怎么可能知道我的fetch 调用会返回什么数据?所以,让我们用一个小小的类型注释来帮助TypeScript编译器吧:
type JSONResponse = {
data?: {
pokemon: Omit<PokemonData, 'fetchedAt'>
}
errors?: Array<{message: string}>
}
const {data, errors}: JSONResponse = await response.json()
现在我们可以删除errors.map 上的显式类型,这很好!
const error = new Error(errors?.map(e => e.message).join('\n') ?? 'unknown')
注意那里使用的是Omit 。因为fetchedAt 属性在我们的PokemonData ,但它不是来自于API,所以说它是来自于TypeScript和未来的代码读者的谎言(我们应该避免这样)。
用TypeScript进行Monkey-patching
有了这些,我们现在会得到两个新的错误:
// add fetchedAt helper (used in the UI to help differentiate requests)
pokemon.fetchedAt = formatDate(new Date())
return pokemon
像这样给一个对象添加新的属性,通常被称为 "猴子补丁"。
第一个是针对pokemon.fetchedAt ,它说。
Property 'fetchedAt' does not exist on type 'Pick<PokemonData, "number" | "id" | "name" | "image" | "attacks">'. ts(2339)
第二个是针对return pokemon ,上面写着。
Property 'fetchedAt' is missing in type 'Pick<PokemonData, "number" | "id" | "name" | "image" | "attacks">' but required in type 'PokemonData'. ts(2741)
好吧,大声哭出来吧,TypeScript,第一个是抱怨fetchedAt 不应该存在,而第二个是说它应该存在!请你决定吧!😩
我们总是可以告诉TypeScript去管,并使用一个类型断言将pokemon 投入到一个完整的PokemonData 。但我发现了一个更简单的解决方案。
// add fetchedAt helper (used in the UI to help differentiate requests)
return Object.assign(pokemon, {fetchedAt: formatDate(new Date())})
这使得这两个错误都消失了。Object.assign ,将对象属性组合到目标对象(第一个参数)上,并返回该目标对象。这让编译器很高兴,因为它可以检测到pokemon ,进去时没有fetchedAt ,出来时有fetchedAt 。
如果你感到好奇,这里是Object.assign 的类型定义。
assign<T, U>(target: T, source: U): T & U;
就是这样!我们现在已经为一个特定的请求成功地输入了fetch 。🎉
为承诺的拒绝值打字
在这里有一个最后的教训。不幸的是,Promise 类型泛型只接受resolved 的值,而不接受rejected 的值。所以我做不到。
async function fetchPokemon(name: string): Promise<PokemonData, Error> {}
原来,这与我的另一个挫折有关。
try {
throw new Error('oh no')
} catch (error: Error) {
// ^^^^^ Catch clause variable type annotation
// must be 'any' or 'unknown' if specified.
// ts(1196)
}
原因是错误可能因为完全意想不到的原因而发生。TypeScript认为你不可能知道是什么触发了错误,所以你不能知道错误会是什么类型。
这有点让人扫兴,但也可以理解。
总结
好了,这里是最终版本。
const formatDate = (date: Date) =>
`${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')} ${String(
date.getSeconds(),
).padStart(2, '0')}.${String(date.getMilliseconds()).padStart(3, '0')}`
type PokemonData = {
id: string
number: string
name: string
image: string
fetchedAt: string
attacks: {
special: Array<{
name: string
type: string
damage: number
}>
}
}
async function fetchPokemon(name: string): Promise<PokemonData> {
const pokemonQuery = `
query PokemonInfo($name: String) {
pokemon(name: $name) {
id
number
name
image
attacks {
special {
name
type
damage
}
}
}
}
`
const response = await window.fetch('https://graphql-pokemon2.vercel.app/', {
// learn more about this API here: https://graphql-pokemon2.vercel.app/
method: 'POST',
headers: {
'content-type': 'application/json;charset=UTF-8',
},
body: JSON.stringify({
query: pokemonQuery,
variables: {name: name.toLowerCase()},
}),
})
type JSONResponse = {
data?: {
pokemon: Omit<PokemonData, 'fetchedAt'>
}
errors?: Array<{message: string}>
}
const {data, errors}: JSONResponse = await response.json()
if (response.ok) {
const pokemon = data?.pokemon
if (pokemon) {
// add fetchedAt helper (used in the UI to help differentiate requests)
return Object.assign(pokemon, {fetchedAt: formatDate(new Date())})
} else {
return Promise.reject(new Error(`No pokemon with the name "${name}"`))
}
} else {
// handle the graphql errors
const error = new Error(errors?.map(e => e.message).join('\n') ?? 'unknown')
return Promise.reject(error)
}
}
我希望这对你来说是有趣和有用的祝你好运。