以下是 effective-typescript 仓库代码阅读后的总结,想详细了解可以查看上述仓库代码。
一、了解TypeScript
1. 了解TypeScript与Javascript之间的联系
- typescript允许给变量标记类型,javascript没有
function greet(who: string) { console.log('Hello', who); }
- typescript会对调用错误的函数报错,javascript没有
let city = 'new york city'; console.log(city.toUppercase()); // // ~~~~~~~~~~~ Property 'toUppercase' does not exist on type // 'string'. Did you mean 'toUpperCase'?
- typescript会对变量进行类型推断,当调用不存在的属性时会报错,javascript不会
export const foo = true; const states = [ {name: 'Alabama', capital: 'Montgomery'}, {name: 'Alaska', capital: 'Juneau'}, {name: 'Arizona', capital: 'Phoenix'}, // ... ]; // END for (const state of states) { console.log(state.capitol); // ~~~~~~~ Property 'capitol' does not exist on type // '{ name: string; capital: string; }'. // Did you mean 'capital'? }
- typescript中的变量类型不匹配时会报错,javascript没有
interface State {
name: string;
capital: string;
}
const states: State[] = [
{name: 'Alabama', capitol: 'Montgomery'},
// ~~~~~~~~~~~~~~~~~~~~~
{name: 'Alaska', capitol: 'Juneau'},
// ~~~~~~~~~~~~~~~~~
{name: 'Arizona', capitol: 'Phoenix'},
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known
// properties, but 'capitol' does not exist in type
// 'State'. Did you mean to write 'capital'?
// ...
];
- typescript能识别合法的变量加减,同时也识别出合法的函数调用;javascript没有
const a = null + 7; // Evaluates to 7 in JS
// ~~~~ Operator '+' cannot be applied to types ...
const b = [] + 12; // Evaluates to '12' in JS
// ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript'); // alerts "Hello"
// ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2
- typescript 对于隐式的函数调用错误不能识别,javascript也不能识别
const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase()); // ts能编译通过,到最后执行js脚本时才会报错
2. 了解正在使用的typescript配置(noImplicitAny、strictNullChecks)
noImplicitAny: 如果某些变量没有给类型,typescript会降类型默认回落成
any
类型。当打开noImplicitAny
配置时,无论哪里有any
类型,typescript就会报错。
// "compilerOptions": {"noImplicitAny":true,}
function add (a, b ) {
// ~ Parameter 'a' implicitly has an 'any' type
// ~ Parameter 'b' implicitly has an 'any' type
return a + b;
}
strictNullChecks: 当
strictNullChecks
属性为false
,null
和undefined
会被忽略。这会导致问题。当值为true
时,null
和undefined
有他们直属类型。如果已经明确数据类型的变量用他们赋值,就会报类型错误。
// "compilerOptions": {"strictNullChecks":true}
export const x: number = null;
// ~ Type 'null' is not assignable to type 'number'
export const x: number | null = null; // 给出明确类型,不会报错
3. 明白代码和类型是隔离的
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
// ~~~~~~~~~ 'Rectangle' only refers to a type,
// but is being used as a value here
return shape.width * shape.height;
// ~~~~~~ Property 'height' does not exist
// on type 'Shape'
} else {
return shape.width * shape.width;
}
}
4. 使用舒适的类型结构
// { "noImplicitAny":true, "strictNullChecks":false }
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
name: string;
x: number;
y: number;
}
interface Vector3D {
x: number;
y: number;
z: number;
}
// 类型报错
export function calculateLengthL1(v: Vector3D) {
let length = 0;
for (const axis of Object.keys(v)) {
const coord = v[axis];
// ~~~~~~~ Element implicitly has an 'any' type because ...
// 'string' can't be used to index type 'Vector3D'
length += Math.abs(coord);
}
return length;
}
// 改造之后无类型报错
export function calculateLengthL1(v: Vector3D) {
let length = 0;
for(const axis of Object.keys(v) as Array<keyof Vector3D>) {
const coord = v[axis];
length +=Math.abs(coord);
}
return length;
}
5. 危险的any
let age: number;
age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
age = '12' as any; // OK
为什么有说any
危险呢?
let age: number;
age = '12' as any; // OK
age += 1; // OK; at runtime, age is now "121" 跟我们想要的 12 + 1 = 13 想去甚远
所以除非不得不这样做,不要轻易将类型设置为any。
二、TypeScript的类型系统
1. 喜欢类型声明多于类型断言
interface Person { name: string };
const alice: Person = { name: 'Alice' }; // Type is Person
const bob = { name: 'Bob' } as Person; // Type is Person
类型断言会存在问题,除非是十分确定的类型:
interface Person { name: string };
const alice: Person = {};
// ~~~~~ Property 'name' is missing in type '{}'
// but required in type 'Person'
const bob = {} as Person; // No error 正常能编译通过
也不是所有类型都可以完全断言成另外一个类型:
interface Person { name: string; }
const body = document.body;
const el = body as Person;
// ~~~~~~~~~~~~~~ Conversion of type 'HTMLElement' to type 'Person'
// may be a mistake because neither type sufficiently
// overlaps with the other. If this was intentional,
// convert the expression to 'unknown' first
const el = body as unknown as Person; // 类型断言正确,能正常编译
2. 避免上转型类型(String,Number,Boolean,Symbol,BigInt)
function getStringLen(foo: String) {
return foo.length;
}
getStringLen("hello"); // OK
getStringLen(new String("hello")); // OK
function isGreeting(phrase: String) {
return [
'hello',
'good day'
].includes(phrase);
// ~~~~~~
// Argument of type 'String' is not assignable to parameter
// of type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object;
// prefer using 'string' when possible
// 编译不过,会报错
}
3. 多余属性校验的限制
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r: Room = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
// and 'elephant' does not exist in type 'Room'
// 此处报错,因为Room接口里没有elephant属性
};
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const _r = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present', // ok
};
const r: Room = _r; // 逃避了多余属性的检查
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const _r = {
numDoors: 1,
elephant: 'present', // ok
};
const r: Room = _r;
//~~~~~~~~~~~~~~~~~ 报错:缺少属性ceilingHeightFt
4. 函数类型
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;
5. type 和 interface的区别
共同点:
- 都可以描述函数
type TFn = (x: number) => string;
interface IFn {
(x: number): string;
}
const toStrT: TFn = x => '' + x; // OK
const toStrI: IFn = x => '' + x; // OK
- 都可以描述对象
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
- 都很方便进行扩展
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
interface IStateWithPop extends TState {
population: number;
}
type TStateWithPop = IState & { population: number; };
- 都可以被class实现
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StateI implements IState {
name: string = '';
capital: string = '';
}
不同点:
- interface声明可以重复,类型定义会进行融合
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}; // OK
// IState 融合了 name、capital和population属性
- type 可以定义基础类型
type T = 1 | 2 | 3
const t: T = 3; // ok
6. 避免重复声明
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
type TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
};
// 或者
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
// 或者
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
直接用初始值推断出来的类型:
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
type Options = typeof INIT_OPTIONS;
// 类型 type Options = {
// width: number;
// height: number;
// color: string;
// label: string;
}
利用函数返回值推断
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
function getUserInfo(userId: string) {
// COMPRESS
const name = 'Bob';
const age = 12;
const height = 48;
const weight = 70;
const favoriteColor = 'blue';
// END
return {
userId,
name,
age,
height,
weight,
favoriteColor,
};
}
// Return type inferred as { userId: string; name: string; age: number, ... }
type UserInfo = ReturnType<typeof getUserInfo>;
// 类型 type UserInfo = {
// userId: string;
// name: string;
// age: number;
// height: number;
// weight: number;
// favoriteColor: string;
//}
7. 对动态数据采用索引标签
type Rocket = {[property: string]: string};
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
}; // OK
export {}
// 或者
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type ABC = {
// a: number;
// b: string;
// c: number;
// }
三、类型推断
1. 别让推断出来的类型混乱你的代码
interface Product {
id: string;
name: string;
price: number;
}
function logProduct(product: Product) {
const id:string = product.id;
// ~~ Type 'string' is not assignable to type 'number'
const name: string = product.name;
const price: number = product.price;
console.log(id, name, price);
}
const furby: Product = {
name: 'Furby',
id: 630509430963,
// ~~ Type 'number' is not assignable to type 'string'
price: 35,
};
logProduct(furby);
// 推断出来的furby类型为 { name: string, id: number, price: number } 与 Product中的id:string类型不匹配。
2. 理解类型扩大
interface Vector3 { x: number; y: number; z: number; }
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis];
}
let x = 'x'; // let x变量可修改
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x);
// x 类型扩大成了string类型,不在是'x'类型
// ~ Argument of type 'string' is not assignable to
// parameter of type '"x" | "y" | "z"'
// 修改
interface Vector3 { x: number; y: number; z: number; }
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis];
}
const x = 'x'; // type is "x"
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x); // OK
3. 理解类型缩小
const el = document.getElementById('foo'); // Type is HTMLElement | null
if (el) {
el // Type is HTMLElement
el.innerHTML = 'Party Time'.blink();
} else {
el // Type is null
alert('No element #foo');
}
4. 一次性创建对象中所需的所有属性
interface Point { x: number; y: number; }
const pt: Point = {};
// error
// ~~ Type '{}' is missing the following properties from type 'Point': x, y
pt.x = 3;
pt.y = 4;
// modify
interface Point { x: number; y: number; }
const pt:Point = {
x: 3,
y: 4,
}; // ok
四、any
1. 更精确的any比普通的any要好
// 不好
function getLengthBad(array: any) { // Don't do this!
return array.length;
}
// 好
function getLength(array: any[]) {
return array.length;
}