TypeScript中广泛使用类型推断。如果使用得当,可以大大减少你的代码所需要的类型标注的数量,以获得完整的类型安全体验。辨别TypeScript初学者和有经验用户的最简单的一个方法就是看其使用类型标注的数量。一个有经验的TypeScript开发者会使用相对较少的标注(但使用它们会有很大的效果),而一个初学者可能会把他们的代码淹没在多余的类型标注中。
要记住的事情:
- 当TypeScript可以推断出相同的类型时,要避免写类型标注
- 理想情况下,你的代码在函数/方法签名中应该有类型标注,但在它们的主体中的局部变量上没有
- 考虑为对象字面量和函数返回类型使用显式标注,即使它们可以被推断出来。这有助于防止实现错误出现在使用者代码中
在TypeScript中,许多标注是不必要的。为所有的变量声明类型会适得其反,还会被认为是糟糕的风格。
let x: number = 12; // 不要这样写
let x = 12;
显式类型标注是多余的,写它只会增加干扰。如果你不确定某个类型,可以在你的编辑器中进行检查。
TypeScript也会推断出更复杂对象的类型。
const person: {
name: string;
born: {
where: string;
when: string;
};
died: {
where: string;
when: string;
}
} = {
name: 'Bob',
born: {
where: 'New York';
when: 'c.1791';
};
died: {
where: 'New York';
when: 'Nov. 26, 1883'
}
};
// 不如直接写成:
const person = {
name: 'Bob',
born: {
where: 'New York';
when: 'c.1791';
};
died: {
where: 'New York';
when: 'Nov. 26, 1883'
}
};
同样,这两种类型也是完全一样的。把类型写在值之外,只是在这里增加干扰。
允许类型推断也方便重构。例如,你有一个Product类型和它的日志函数:
interface Product {
id: number;
name: string;
price: number;
}
function logProduct(product: Product) {
const id: number = product.id;
const name: string= product.name;
const price: number = product.price;
console.log(id, name, price);
}
在某个时候,你了解到产品ID中除了数字之外,还可能有字母。所以,你改变了Product中id的类型。但因为你把logProduct中所有的变量都附加了显式标注,这就产生了一个错误:
interface Product {
id: string;
name: string;
price: number;
}
function logProduct(product: Product) {
const id: number = product.id; // ~~不能将类型“string”分配给“number”
const name: string= product.name;
const price: number = product.price;
console.log(id, name, price);
}
如果你不在logProduct函数体中使用任何的类型标注,代码就会原封不动地通过类型检查器。更好的logProduct实现是使用解构赋值:
function logProduct(product: Product) {
const { id, name, price } = product;
console.log(id, name, price);
}
这种做法会推断所有局部变量的类型。相应地,带有显式类型标注的做法就有些重复且杂乱无章。
function logProduct(product: Product) {
const { id, name, price }: { id: string; name: string; price: number } = product;
console.log(id, name, price);
}
在某些情况下,当TypeScript没有足够的上下文来确定一个类型时,任然需要明确的类型标注。比如函数参数
理想的TypeScript代码包括函数/方法签名的类型标注,但不包括为其函数体中所创建的局部变量的类型标注。这样可以将干扰降到最低,让我们专注于实现逻辑。
在某些情况下,你也可以不对参数进行类型标注。如参数有默认值时:
function parseNumber(str: string, base=10) { /*...*/ } // 类型base被推断为number,因为默认值是10.
当函数被用作带有类型声明的库的回调时,通常可以推断出其参数类型。比如:
// 不要这样做:
app.get('/health', (request: express.Request, response: express.Response) => {
response.send('OK');
});
// 要这样做
app.get('/health', (request, response) => {
response.send('OK');
});
有些情况下,你可能仍想指定一个类型,即使它可以被推断出来,这样做的好处是会触发额外属性检查,帮助我们捕捉错误,特别是对于有可选字段的类型。
const elmo: Product = {
name: 'Efficent TypeScript',
id: '03812 24352',
price: 28.12
}
类似的考虑也适用于函数的返回类型。即使是在可以推断的情况下,仍然可以对其进行标注,以确保实现的错误不会遗留到函数使用中。比如:
function getQuote(ticker: string) {
return fetch(`https://quotes.example.com/?q=${ticker}`)
.then(response => response.json());
}
你决定添加一个缓存来避免重复的网络请求:
const cache: {[ticker: string]: number} = {};
function getQuote(ticker: string) {
if (ticker in cache) {
return cache[ticker];
}
return fetch(`https://quotes.example.com/?q=${ticker}`)
.then(response => response.json())
.then(quote => {
cache[ticker] = quote;
return quote;
});
}
在这个实现中存在一个失误,你其实应该返回Promise.resolve(cache[ticker]),使得getQuote总是返回一个Promise,这个失误很可能会产生一个错误…但是,该错误出现在调用getQuote的地方,而不是在getQuote本身:
getQuote('MFST').then(considerBuying);
// ~~ 类型“number | Promise<any>”上不存在属性“then”
但如果你标注了期望的返回类型(Promise),错误提示就会出现在正确的地方。
写出返回类型也有可能帮助你更清楚地思考你的函数:你应该在实现它之前知道它的输入和输出类型是什么。虽然实现可能会变, 但函数的契约(类型签名)一般不应该变。
标注返回值的最后一个原因是,你想使用一个具名类型。例如,你可以选择不为这个函数写一个返回类型:
interface Vector2D { x: number; y: number; }
function add(a: Vector2D, b: Vector2D) {
return { x: a.x + b.x, y: a.y + b.y };
}
TypeScript推断返回类型为{ x: number; y: number; }。这与Vector2D是兼容的,但当使用者看到输入的类型是Vector2D而输出的类型不是时,可能会感到惊讶。
如果你对返回值类型进行标注,那么其呈现就会更加直接;而且如果你写了关于类型的文档,那么它也会与返回值关联。随着推断返回类型的复杂性的增加,提供一个名字会更加有用。