effective-typescript总结

142 阅读7分钟

以下是 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,nullundefined会被忽略。这会导致问题。当值为true时,nullundefined有他们直属类型。如果已经明确数据类型的变量用他们赋值,就会报类型错误。

// "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;
}