# 基础语法篇

# Why TS

# 静态类型检查(Static type-checking)

# 非异常失败(Non-exception 失败)

  • 读对象不存在的属性
  • 拼写错误
  • 函数未被调用 / 逻辑错误 ..

# 类型工具(Types for Tooling)

  • 是否正确获取了一个变量的属性

  • 函数返回类型是否正确

  • 描述复杂数据

    • 使用 interface

      interface IParamA {
          id: number;
          name: string;
      }
      
      function fetchList(params: IParamA): Promise<void> {
          return axios.get("/api/demo", params);
      }
      
      fetchList({ id: 1, name: "alex" });
      
    • 枚举类型

      enum Direction {
          up = 1,
          down,
          left,
          right,
      }
      
      type MoveType = "linear" | "ease" | "bounce";
      
      function moveTo(ease: MoveType, direction: Direction): void {
        console.log(ease, direction);
      }
      
  • 描述函数规则,即明确的告诉使用者该函数的参数类型与返回值类型

# tsc / TypeScript 编译器

  • 默认报错仍产出文件(Emitting with Errors)/
  • 报错不产出文件:tsc --noEmitOnError hello.ts
  • 编译前 - 显式类型注解(type annotations) / 自动推导类型
  • 编译后 - 类型抹除(Erased Types)
    • 编译后会语法降级为 es3
    • --target esxxx 指定编译后语法版本
  • 严格模式(Strictness) - --noImplicitAny 在某些时候,TypeScript 并不会为我们推断类型,这时候就会回退到最宽泛的类型:any。开启后,当类型被隐式推断为 any 时,会抛出一个错误
  • --strictNullChecks 默认情况下,像 null 和 undefined 这样的值可以赋值给其他的类型。开启后,让我们更明确的处理 null 和 undefined

# 总结

ts 是一套语法规则,主要作用在于约束,帮助我们约束自己的代码规范。

# 基础语法

# 基础原始类型

  • string、number、boolean
  • String ,Number 和 Boolean (首字母大写)也是合法的,但它们是一些非常少见的特殊内置类型

# 数组

  • 声明特定类型的数组:number[] / Array<number>

# any

  • 当一个值是 any 类型的时候,可获取它任意属性 (也会被转为 any 类型)
  • 像函数一样调用它,把它赋值给一个任意类型的值
  • 把任意类型的值赋值给它
  • 其他 JS 语法正确的操作
  • 若无指定一个类型,TypeScript 也不能从上下文推断出它的类型,编译器就会默认设置为 any 类型。(noImplicitAny 可避免)

# 对象

  • 列出属性名称和对应的类型
  • 可选属性
function printName(obj: { first: string; last?: string }) {}
  • object / Object / {}

    • object 用于表示非原始类型

      let objectCase: object;
      objectCase = {}; // ok
      objectCase = 1; // error
      objectCase = "a"; // error
      objectCase = true; // error
      objectCase = null; // error
      
    • Object

    1. 所有拥有 toStringhasOwnProperty 方法的类型
    2. 所有原始类型、非原始类型都可以赋给 Object
    let ObjectCase: Object;
    ObjectCase = {}; // ok
    ObjectCase = 1; // ok
    ObjectCase = "a"; // ok
    ObjectCase = true; // ok
    ObjectCase = null; // error
    
    • {}:和大Object 一样,也表示原始类型和非原始类型的集合
    let simpleCase: {};
    simpleCase = {}; // ok
    simpleCase = 1; // ok
    simpleCase = "a"; // ok
    simpleCase = true; // ok
    simpleCase = null; // error
    simpleCase = undefined; // error
    

# 联合类型(Union Types)

  • 基于已经存在的类型构建新的类型
  • 定义联合类型:由两个或者更多类型组成的类型,表示值可能是这些类型中的任意一个。
function printId(id: number | string) {}

# 类型别名(Type Aliases)

一个类型会被使用多次,更希望通过一个单独的名字来引用它

type Point = { x: number; y: number };
type ID = string | number;
function printCoord(pt: Point) {
    console.log("The value is " + pt.x + "," + pt.y);
}

# 类型断言(Type Assertions)

  • 使用类型断言将其指定为一个更具体的类型
  • 用于你清楚地知道一个实体具有比它现有类型更确切的类型。其实就是你需要手动告诉 ts 就按照你断言的那个类型通过编译
// as 语法(推荐
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

// 尖括号语法 (和rxjs冲突
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

# 非空断言

  • 在上下文中当类型检查器无法断定类型,可以通过 ! 断言操作对象是非 null 和非 undefined 类型

    let flag: null | undefined | string;
    flag!.toString(); // ok
    flag.toString(); // error
    
  • 用于你清楚地知道一个实体具有比它现有类型更确切的类型。其实就是你需要手动告诉 ts 就按照你断言的那个类型通过编译

# 字面量类型(Literal Types)

  • 字面量是 JS 本身提供的一个准确变量(不仅可以表示值,还可以表示类型)。在 TS 中,可以使用一个字符串字面量作为一个类型。
// 创建了一个被称为 foo 变量,它仅接收一个字面量值为 Hello 的变量
let foo: "Hello";
foo = "Bar"; // Error: 'bar' 不能赋值给类型 'Hello'
  • TS 还提供了 boolean 和 number 的字面量类型
  • 结合联合类型使用
// 当函数只能传入一些固定的字符串
type Alignment = "left" | "right" | "center";
function printText(s: string, alignment: Alignment) {}
printText("Hello, world", "left"); // ok
printText("G'day, mate", "centre"); //error
  • 字面量推断(Literal Inference)
function iTakeFoo(foo: "foo") {}
const test = { prop: "foo" };

// 由于 test 会被推断为 { prop: string }
// 即test.prop会被推断为string而非'foo',所以导致这个错误
// Error: Argument of type string is not assignable to parameter of type 'foo'
iTakeFoo(test.prop);
  • 解决方法

    • 添加一个类型断言改变推断结果
        // Change 1: 有意让 test.prop  的类型为字面量类型 "foo"
    const  test = { prop:  "foo" as "foo" };
    
    // Change 2:“我知道test.prop 的值是 foo”.
    iTakeFoo(test.prop as "foo");
    
    • 使用类型注解的方式,帮助推断正确的类型
    type Test = {someProp: 'foo';};
    
    //  推断 `someProp` 永远是 'foo'
    const test: Test = {someProp: 'foo'};
    
    • xx as const 把整个对象转为一个具体的类型字面量
  • 更多用法:https://jkchao.github.io/typescript-book-chinese/typings/literals.htm

# 类型收窄 (type narrowing)

将类型推导为更精确类型的过程

# 类型保护

  • 通过 typeof 检查返回的值
// 参数 padding 是一个数字,我们就在 input 前面添加同等数量的空格; padding 是一个字符串就直接添加到 input 前面
function padLeft(padding: number | string, input: string) {
    if (typeof padding === "number") {
        return new Array(padding + 1).join(" ") + input;
    }
    return padding + input;
}

# 真值收窄

  • 通过!操作符
function multiplyAll(
    values: number[] | undefined,
    factor: number
): number[] | undefined {
    if (!values) {
        return values; // (parameter) values: undefined
    } else {
        return values.map((x) => x * factor); // (parameter) values: number[]
    }
}

# 等值收窄

通过 switch 语句和等值检查比如 === / !== 去收窄类型

# in 操作收窄

// value 可以为一个字面量或者属性
"value" in type;

# instanceof 收窄

class Animal {
    name!: string;
}
class Bird extends Animal {
    fly!: number;
}
function getName(animal: Animal) {
    if (animal instanceof Bird) {
        console.log(animal.fly);
    } else {
        console.log(animal.name);
    }
}

# 函数

  • 参数类型注解(Parameter Type Annotations)
function greet(name: string) {}
  • 返回值类型注解(Return Type Annotations)
function getFavoriteNumber(): number {
    return 26;
}
  • 匿名函数(Anonymous Functions)
    当 TypeScript 知道一个匿名函数将被怎样调用的时候,匿名函数的参数会被自动的指定类型。

  • 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。在 TypeScript 中,表现为给同一个函数提供多个函数类型定义

let obj: any = {};
function attr(val: string): void;
function attr(val: number): void;
function attr(val: any): void {
    if (typeof val === "string") {
        obj.name = val;
    } else {
        obj.age = val;
    }
}
attr("hahaha");
attr(9);
attr(true);
console.log(obj);

注意:函数重载真正执行的是同名函数最后定义的函数体 在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法 只能定义类型

# 接口(Interfaces)

命名对象类型的另一种方式,eg:

interface Point {
    x: number;
    y: number;
}

# 类型别名和接口的差异

  • 接口和类型别名非常相似,接口的几乎所有特性都可以在 type 中使用
  • 两者最关键的差别在于类型别名本身无法添加新的属性,而接口可以扩展
// 通过继承扩展类型
interface Animals {
    name: string;
}

interface Bear extends Animals {
    color: string;
}

function getBear(bear: Bear) {
    console.log(bear.name, bear.color);
}

// error: Property 'color' is missing in type '{ name: string; }' but required in type 'Bear'.
// getBear({name: 'bear'})

getBear({ name: "bear", color: "white" });

// 通过交集扩展类型
type Animals = { name: string };
type Bear = Animals & { color: string };
  • 类型别名一旦定义无法修改,而接口可以
// Interface 对一个已经存在的接口添加新的字段
interface Window {
    title: string;
}

interface Window {
    ts: TypeScriptAPI;
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
// type创建后不能被改变
type Window = { title: string };

// Error: Duplicate identifier 'Window'.
type Window = { ts: TypeScriptAPI };

# 泛型

泛型,即为更广泛的约束类型。

在比如 C# 和 Java 语言中,用来创建可复用组件的工具,我们称之为泛型(generics)。利用泛型,我们可以创建一个支持众多类型的组件,这让用户可以使用自己的类型消费(consume)这些组件。

🌰 例子 1:定义一个恒等函数,返回传入的参数

// 给恒等函数加上了一个类型变量 Type,这个 Type 允许我们捕获用户提供的类型,使得我们在接下来可以使用这个类型。这里,我们再次用 Type 作为返回的值的类型
function identify<Type>(input: Type): Type {
    return input;
}

// 使用
// eg1,第一种方式是传入所有的参数,包括类型参数
const eg1 = identify<string>("test");
console.log("eg1", eg1);

// eg2,类型参数推断(type argument inference)
const eg2 = identify("test auto type");
console.log("eg2", eg2);

🌰 例子 2:描述 Array 类型与数组中的 map 方法

interface Person {
    name: string;
    age: number;
}

const demo1: number[] = [1, 2, 3];
const demo2: string[] = ["a", "b", "c"];
const demo3: Person[] = [
    { name: "alex", age: 20 },
    { name: "john", age: 10 },
    { name: "hx", age: 21 },
];

demo1.map((item) => item); // item: number
demo2.map((item) => item); // item: string
demo3.map((item) => item); // item: Person

当不同的数组调用 map 时,回调函数的参数 item,会自动推导为对应的数据类型。也就是说,这里的 item,必然是使用了泛型进行了更为宽松的约束。

具体如下:

interface Array<T> {
    map<U>(callbckFn: (value: T, index: number, arr: T[]) => U): U[];
}

我们在声明数组类型时,定义了一个泛型变量T。其中,T作为泛型变量的含义为:我们在定义约束条件时,暂时还不知道数组的每一项数据类型到底是什么,因此我们只能放一个占位标识在这里,待具体使用时再来明确每一项的具体类型。

因此针对数据的描述,我们通常可以在声明市明确泛型变量 T 的具体数据类型,分别对应为 number, string, Person。

const demo1: Array<number> = [1, 2, 3];
const demo2: Array<string> = ["a", "b", "c"];
const demo3: Array<Person> = [
    { name: "alex", age: 20 },
    { name: "john", age: 10 },
    { name: "hx", age: 21 },
];

# 泛型用法

  • 函数中使用泛型
// 声明一个泛型变量T,并在参数中和返回值中使用这个泛型变量
function identity<T>(arg: T): T {}

// 变量声明函数的写法
let myIdentity: <T>(arg: T) => T = identity;
  • 接口中使用泛型
// 使用接口约束一部分数据类型,使用泛型变量让剩余部分变得灵活
interface Parseer<T> {
    success: boolean;
    result: T;
    code: number;
    desc: string;
}

// 接口泛型与函数泛型结合
interface Array<T> {
    map<U>(callbackfn: (value: T, index: number, array: T[]) => U): U[];
}
  • class 中使用泛型
declare namespace demo02 {
    class GenericNumber<T> {
        private value: T;
        public add: (x: T, y: T) => T;
    }
}

// 多个泛型变量传入
declare namespace demo02 {
    class Component<P, S> {
        private constructor(props: P);
        public state: S;
    }
}

//eg
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
    return x + y;
};
  • 在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
  • 再来一个实践场景

描述数组

interface Array<T> {
    length: number;
    toString(): string;
    pop(): T | undefined;
    // 注意此处的含义
    push(...items: T[]): number;
    concat(...items: T[]): T[];
    join(separator?: string): string;
    reverse(): T[];
    shift(): T | undefined;
    slice(start?: number, end?: number): T[];
    sort(compareFn?: (a: T, b: T) => number): this;
    splice(start: number, deleteCount?: number): T[];
    // 注意此处的重载写法
    splice(start: number, deleteCount: number, ...items: T[]): T[];
    unshift(...items: T[]): number;
    indexOf(searchElement: T, fromIndex?: number): number;
    lastIndexOf(searchElement: T, fromIndex?: number): number;
    every(
        callbackfn: (value: T, index: number, array: T[]) => boolean,
        thisArg?: any
    ): boolean;
    some(
        callbackfn: (value: T, index: number, array: T[]) => boolean,
        thisArg?: any
    ): boolean;
    forEach(
        callbackfn: (value: T, index: number, array: T[]) => void,
        thisArg?: any
    ): void;
    map<U>(
        callbackfn: (value: T, index: number, array: T[]) => U,
        thisArg?: any
    ): U[];

    filter<S extends T>(
        callbackfn: (value: T, index: number, array: T[]) => value is S,
        thisArg?: any
    ): S[];

    filter(
        callbackfn: (value: T, index: number, array: T[]) => any,
        thisArg?: any
    ): T[];

    reduce(
        callbackfn: (
            previousValue: T,
            currentValue: T,
            currentIndex: number,
            array: T[]
        ) => T
    ): T;

    reduce(
        callbackfn: (
            previousValue: T,
            currentValue: T,
            currentIndex: number,
            array: T[]
        ) => T,
        initialValue: T
    ): T;

    reduce<U>(
        callbackfn: (
            previousValue: U,
            currentValue: T,
            currentIndex: number,
            array: T[]
        ) => U,
        initialValue: U
    ): U;
    // reduceRight 略
    // 索引调用
    [n: number]: T;
}

#