TypeScript学习笔记

143 阅读14分钟

常用类型

基元类型 string、number 和 boolean

let num: number = 123;
let str: string = 'abc'
let bool: boobean = true

数组

// 定义数组的两种方法
let arr:string[] = ['a','b','c']
let arr2:Array<number> = [1,2,3]

any

TypeScript 还有一个特殊类型 any,当你不希望某个特定值导致类型检查错误时,可以使用它。 当一个值的类型是 any 时,可以访问它的任何属性,将它分配给任何类型的值,或者几乎任何其他语法上的东西都合法的:

let obj: any = { x: 0 };
// 以下代码行都不会抛出编译器错误。
// 使用'any'将禁用所有进一步的类型检查
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

但在运行环境下执行代码可能是错误的。

当你不想写出长类型只是为了让 TypeScript 相信特定的代码行没问题时,any 类型很有用。

noImplicitAny,当不指定类型时,并且 TypeScript 无法从上下文推断它时,编译器通常会默认为 any . 但是,您通常希望避免这种情况,因为 any 没有进行类型检查。使用编译器标志 noImplicitAny 将任何 隐式标记 any 为错误。

变量上的类型注释

当你使用 const、var,let 或者声明变量时,可以选择添加类型注释来显式指定变量的类型: let name:string = "zhangsan" 但是,在大多数情况下,显式指定变量不是必须的。只要有可能,TypeScript 就会尝试自动推断代码中的类型。

函数

函数是在 JavaScript 中传递数据的主要方式。TypeScript 允许你指定函数的输入和输出值的类型。

// 参数类型定义
function greet(name: string){
    console.log("hello," + name.toUpperCase() + '!!');
}
// 传入错误的类型参数时,TypeScripth会错误提示
greet(123); // 报错
// 返回类型注释
function getAge(): number {
    return 28;
}

匿名函数 匿名函数与函数声明有点不同。当一个函数出现在 TypeScript 可以确定它将如何调用的地方时,改函数就会自动指定类型。

const names = ['zhangsan','lisi','wangwu'];
// 函数上下文类型
names.forEach(function(s){
    console.log(s.toUpperCase());
})
// 函数上下文类型也适用于箭头函数
names.forEach((s)=>{
    console.log(s.toUpperCase());
})

对象类型

// 参数的类型注释是对象类型
function printCoord(pt: {x: number, y: number}){
    console.log('坐标的X轴为:' + pt.x);
    console.log('坐标的Y轴为:' + pt.y);
}
printCoord({x: 3,y: 7});

可选属性 对象类型还可以指定其部分或全部属性是可选的。请在属性名称后添加一个?。

function printName(obj: {first: string; last?: string }){
    console.log(obj.last.toUpperCase()); // 错误- 'obj.last'可能不存在

    if(obj.last !== undefined){
        // 这样可以
        console.log(obj.last.toUpperCase());
    }

    // 使用现代JavaScript语法的安全替代方案;
    console.log(obj.last?.toUpperCase());
}
// 两种传递参数都可以
printName({ first: "zhang"});
printName({ first: "zhang", last: "san"})

联合类型

// 参数的类型是联合类型
function printId(Id: number | string){
    if(typeof Id === 'string'){
        // 在此分支中,Id 的类型为string
        console.log(Id.toUpperCase());
    }else{
        // 在此分支中,Id的类型为number
        console.log(Id)
    }
}

类型别名

// 类型别名语法:
type Point = {
    x: number;
    y: number;
};

// 参数中使用类型别名
function printCoord(pt: Point){
    console.log("坐标的X轴为:" + pt.x);
    console.log("坐标的Y轴为:" + pt.y);
}

printCoord({x:100,y:200});

实际上,你可以使用类型别名为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类型:

type ID = string |  number;

请注意,别名只是别名 - 你不能使用类型别名来创建相同类型的不同“版本”。当你使用别名时,就像你编 写了别名类型一样。换句话说,这段代码可能看起来不合法,但根据 TypeScript 是可以的,因为这两种 类型都是同一类型的别名:

type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return str.slice(0, 2)
}
// 创建经过 sanitize 的输入
let userInput = sanitizeInput('hello');
// 但仍可以使用字符串重新分配值
userInput = "new input";

接口

// 接口声明语法
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("坐标x的值是: " + pt.x);
console.log("坐标y的值是: " + pt.y);
}
printCoord({ x: 100, y: 100 });

类型别名和接口之间的差异

类型别名和接口非常相似,在很多情况下你可以自由选择它们。几乎所有的功能都在 interface 中可用 type ,关键区别在于扩展新类型的方式不同:

// 扩展接口
interface Animal {
    name: string
}
interface Bear extends Animal {
    honey: boolean
}
const bear: Bear = {
    name: 'winnie',
    honey: true
}
bear.name
bear.honey
// 通过交叉点扩展类型
type Animal = {
    name: string
}
type Bear = Animal & {
    honey: boolean
}
const bear: Bear = {
    name: 'winnie',
    honey: true
}
bear.name;
bear.honey;
// 向现有接口添加新字段
interface MyWindow {
    title: string
}
interface MyWindow {
    count: number
}
const w: MyWindow = {
    title: 'hello ts',
    count: 100
}
// 类型创建后不可更改
type MyWindow = {
    title: string
}
type MyWindow = {
    count: number
}
  • 在 TypeScript 4.2 版之前,类型别名可能出现在错误消息中,有时会代替等效的匿名类型
  • (这可能是可取的,也可能是不可取的)。接口将始终在错误消息中命名。
  • 类型别名可能不参与声明合并,但接口可以。
  • 接口只能用于声明对象的形状,不能重命名基元。
  • 接口名称将始终以其原始形式出现在错误消息中,但仅当它们按名称使用时。 大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你是否需要其他类型的声明。 如果您想要启发式,请使用 interface ,然后在需要时使用 type 。

类型断言

有时,你会获得 有关TypeScript不知道的值类型的信息。 在这种情况下,你可以使用类型断言来指定具体的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

与类型注释一样,类型断言由编译器删除,不会影响代码的运行时行为。

还可以使用尖括号语法(除非代码在。tsx文件中),他是等效的:

const myCanvas = <HTMLCanvasElemnet>document.getElemntById("main_canvas");

提醒:因为类型断言在编译时被移除,所有没有与类型断言相关联的运行时检查。null如果类型断言错误,编译时则不会出现异常,但是在运行时可能出现异常。

TypeScript只允许类型断言转换为更具体或不太具体的类型版本。此规则可防止“不可能”的强制,例如:

const x = 'hello' as number;

将类型string转换为类型number可能是错误的,因为两种类型都没有充分重叠。如果这是有意的,请先将表达式转换为any或unkonwn,然后再转换为所需的类型:

const x = ('hello' as unknown) as number;

文字类型

除了一般类型string和number,我们可以在类型位置引用特定的字符串和数字。

就其本身而言,文字类型并不是很有价值:

let x: 'hello' = 'hello';
x = 'hello'; // 正确
x = 'howdy'; // 错误

但是通过将文字组合成联合,你可以表达一个更有用的概念。例如:只接受一组特定已知值得函数:

function printText(s:string, alignment: "left" | 'center' | "right"){
    // ...
}
printText('Hello',"left")
printText('Hello',"abc") // 报错

数字文字类型的工作方式相同

function compare(a: string, b: string): -1 | 0 | 1 {
    return a === b ? 0 : a > b ? 1: -1;
}

可以和非文字类型结合使用

interface Options {
    width: number;
}
function configure(x: Options | "auto") {
    // ...
}
configure({width: 100});
configure('auto')
configure('automatic'); // 报错

布尔文字

还有一种文字类型:布尔文字。只有两种布尔文字类型,它们是类型true和false。类型boolean本身实际上只是联合类型union的别名true | false。

文字推理

当你使用对象初始化变量时,TypeScript假定该对象的属性稍后可能会更改值。例如:

const obj = { counter: 0 };
if(someConfition){
    obj.counter = 1;
}

obj.cunter必须有number属性,而非是0,因为类型用于确定读取和写入行为。

这同样适用于字符串:

functon handleRequest(url: string, method: 'GET' | "POST" | "GUESS") {
    // ...
}
const req = { url: 'https://examle.com', method: "GET"};
handleRequest(req.url, req.method); // 报错

在上面的例子中req.method推断是string,不是“GET”。因为代码可以在创建req和调用之间进行评估,TypeScript会认为这段代码有错误。

有两种方法可以解决这个问题: 1、可以通过在任意位置添加类型断言来更改推理:

// 方案1:
const req = {url: 'https://example.com', method: "GET" as "GET"};
// 方案2:
handleRequest(req.url, req.method as "GET");

2、可以使用as const将整个对象转换为类型文字:

const req = {url: "https://example.com", method: "GET"}  as const;
handleRequest(req.url, req.method);

该as const后缀就像const定义,确保所有属性分配的文本类型,而不是一个一般的string或者number。

null和undefined

JavaScript有两个原始值用于表达不存在或未初始化的值: null和undefined。 TypeScript配置中,strictNullChecks 设置为false时,可以将值分配给任何类型的属性。这类似于没有空检查的语言(例如C#、Java)的行为方法。缺乏对这些值的检查往往是错误的主要来源。

设置为true,你需要在对该值使用方法或属性之前测试这些值。就像在使用可选属性之前检查一样,我们可以使用缩小来检查可能的值:

function doSomething(x: string | null){
    if(x === null){
        // ...
    }else{
        console.log("hello," + x.toUpperCase());
    }
}

非空断言运算符(!后缀)

TypeScript也有一种特殊的语法null, undefined, 可以在不进行任何显式检查的情况下,从类型中移除空类型。!在任何表达式之后写入实际上是一种类型断言,既该值不是null 或undefined:

function liveDangerously(x?: number | null) {
    // 正确
    console.log(x!.toFixed());
}

就像其他类型断言一样,这不会更改代码的运行时行为,因此仅!当你明确知道该值不能是null或undefined时才能使用。

枚举

枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量 之一。与大多数 TypeScript 功能不同,这不是JavaScript 的类型级别的添加,而是添加到语言和运行时 的内容。因此,你确定你确实需要枚举在做些事情,否则请不要使用。

// ts源码
enum Direction {
Up = 1,
Down,
Left,
Right,
}
console.log(Direction.Up) // 1

// 编译后的js代码
"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);

不太常见的原语

值得一提的是 JavaScript 中一些较新的原语,它们在 TypeScript 类型系统中也实现了。

bigint

从 ES2020 开始,JavaScript 中有一个用于非常大的整数的原语 BigInt :

// 通过bigint函数创建bigint
const oneHundred: bigint = BigInt(100);
// 通过文本语法创建BigInt
const anotherHundred: bigint = 100n;

symbol

JavaScript 中有一个原语 Symbol() ,用于通过函数创建全局唯一引用:

const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 这里的代码不可能执行
}

此条件将始终返回 false ,因为类型 typeof firstName 和 typeof secondName 没有重叠。

类型缩小

假设我们有一个名为 padLeft 的函数:

function padLeft(padding: number | string, input: string): string {
    throw new Error("尚未实现!");
}

我们来扩充一下功能:如果 padding 是 number ,它会将其视为我们想要添加到 input 的空格数;如 果 padding 是 string ,它只在 input 上做 padding 。让我们尝试实现:

function padLeft(padding: number | string, input: string) {
    return new Array(padding + 1).join(" ") + input; // 报错
}

在 padding + 1 处我们遇到错误。TypeScript 警告我们,运算符 + 不能应用于类型 string | number 和 number ,这是对的。换句话说,我们没有明确检查 padding 是否为 number ,也没有处理它是 string 的情况,所以我们这样做:

function padLeft(padding: number | string, input: string) {
    if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
    }
    return padding + input;
}

虽然看起来不多,但实际上有很多东西在这里。就像TypeScript使用静态类型分析运行时的值一样,它在JavaScript的运行时控制流构造上叠加了类型分析,如if/else、条件三元组、循环、真实性检查等,这些都会影响到这些类型。

在我们的if检查中,TypeScript看到 typeof padding ==="number" ,并将其理解为一种特殊形式的代码,称为类型保护。TypeScript遵循我们的程序可能采取的执行路径,以分析一个值在特定位置的最具体的可能类型。它查看这些特殊的检查(称为类型防护)和赋值,将类型细化为比声明的更具体的类型的过程被称为缩小。在许多编辑器中,我们可以观察这些类型的变化,我们甚至会在我们的例子中这样做。

TypeScript 可以理解几种不同的缩小结构。

typeof类型守卫

正如我们所见,JavaScript 支持一个 typeof 运算符,它可以提供有关我们在运行时拥有的值类型的非常基本的信息。TypeScript 期望它返回一组特定的字符串:

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

就像我们在 padLeft 中看到的那样,这个运算符经常出现在许多 JavaScript 库中,TypeScript 可以理解为,它缩小在不同分支中的类型。

function printAll(strs: string | string[] | null) {
    if (typeof strs === "object") {
        for (const s of strs) {
            console.log(s);
        }
    } else if (typeof strs === "string") {
        console.log(strs);
    } else {
        // 做点事
     }
}

在 printAll 函数中,我们尝试检查 strs 是否为对象,来代替检查它是否为数组类型(现在可能是强调数组是 JavaScript 中的对象类型的好时机)。但事实证明,在 JavaScript 中, typeof null 实际上也是 "object" ! 这是历史上的不幸事故之一。

有足够经验的用户可能不会感到惊讶,但并不是每个人都在 JavaScript 中遇到过这种情况;幸运的是,typescript 让我们知道, strs 只缩小到 string[] | null ,而不仅仅是 string[] 。这可能是我们所谓的“真实性”检查的一个很好的过渡。

真值缩小

真值检查是我们在 JavaScript 中经常做的一件事。在 JavaScript 中,我们可以在条件、 && 、 || 、 if语句、布尔否定 ( ! ) 等中使用任何表达式。例如, if 语句不希望它们的条件总是具有类型 boolean 。

function getUsersOnlineMessage(numUsersOnline: number) {
    if (numUsersOnline) {
        return `现在共有 ${numUsersOnline} 人在线!`;
    }
    return "现在没有人在线. :(";
}

等值缩小

typescript 也使用分支语句做 === , !== , == ,和 != 等值检查,来实现类型缩小。例如:

function example(x: string | number, y: string | boolean) {
    if (x === y) {
        // 现在可以在x,y上调用字符串类型的方法了
        x.toUpperCase();
        y.toLowerCase();
    } else {
        console.log(x);
        console.log(y);
    }
}

in操作符缩小

JavaScript 有一个运算符,用于确定对象是否具有某个名称的属性: in 运算符。TypeScript 考虑到了这一点,以此来缩小潜在类型的范围。

例如,使用代码: "value" in x 。这里的 "value" 是字符串文字, x 是联合类型。值为“true”的分支缩小,需要 x 具有可选或必需属性的类型的值;值为 “false” 的分支缩小,需要具有可选或缺失属性的类型的值。

type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        return animal.swim();
    }
    return animal.fly();
}

另外,可选属性还将存在于缩小的两侧,例如,人类可以游泳和飞行(使用正确的设备),因此应该出现在 in 检查的两侧:

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
    if ("swim" in animal) {
        // animal: Fish | Human
        animal;
    } else {
        // animal: Bird | Human
        animal;
    }
}

函数

函数表达式

描述一个函数的最简单方法是用一个函数类型表达式。这些类型在语法上类似于箭头函数。

function greeter(fn: (a: string) => void) {
    fn("Hello, World");
}
function printToConsole(s: string) {
    console.log(s);
}
greeter(printToConsole);

当然,我们可以用一个类型别名来命名一个函数类型。

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
    // ...
}

调用签名

在JavaScript中,除了可调用之外,函数还可以有属性。然而,函数类型表达式的语法不允许声明属性。 如果我们想用属性来描述可调用的东西,我们可以在一个对象类型中写一个调用签名。

type DescribableFunction = {
    description: string;
    (someArg: number): boolean;
}
function doSomething(fn: DescribableFunction) {
    console.log(fn.description + " returned " + fn(6));
}
function fn1() {
    return true
}
fn1.description = 'balabala...'
doSomething(fn1)

注意,与函数类型表达式相比,语法略有不同:在参数列表和返回类型之间使用 : 而不是 => 。

构造签名

JavaScript函数也可以用 new 操作符来调用。TypeScript将这些称为构造函数,因为它们通常会创建一个新的对象。你可以通过在调用签名前面添加 new 关键字来写一个构造签名。

class Ctor {
    s: string
    constructor(s: string) {
        this.s = s
    }
}
type SomeConstructor = {
    new (s: string): Ctor
}
function fn(ctor: SomeConstructor) {
    return new ctor("hello")
}
const f = fn(Ctor)
console.log(f.s)

有些对象,如 JavaScript 的 Date 对象,可以在有 new 或没有 new 的情况下被调用。你可以在同一类型中任意地结合调用和构造签名。

interface CallOrConstruct {
    new (s: string): Date;
    (n?: number): number;
}

function fn(date: CallOrConstruct) {
    let d = new date('2021-11-20')
    let n = date(100)
}

再举一个例子:

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
    tick(): void;
}

function createClock(
    ctor: ClockConstructor,
    hour: number,
    minute: number
): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) {}
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) {}
        tick() {
            console.log("tick tock");
        }
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

泛型函数

在写一个函数时,输入的类型与输出的类型有关,或者两个输入的类型以某种方式相关,这是常见的。让我们考虑一下一个返回数组中第一个元素的函数。

function firstElement(arr: any[]) {
    return arr[0];
}

这个函数完成了它的工作,但不幸的是它的返回类型是 any 。如果该函数返回数组元素的类型会更好。

在TypeScript中,当我们想描述两个值之间的对应关系时,会使用泛型。我们通过在函数签名中声明一个类型参数来做到这一点:

function firstElement<Type>(arr: Type[]): Type | undefined {
    return arr[0];
}

通过给这个函数添加一个类型参数 Type ,并在两个地方使用它,我们已经在函数的输入(数组)和输出(返回值)之间建立了一个联系。现在当我们调用它时,一个更具体的类型就出来了:

// s 是 'string' 类型
const s = firstElement(["a", "b", "c"]);
// n 是 'number' 类型
const n = firstElement([1, 2, 3]);
// u 是 undefined 类型
const u = firstElement([]);

类型推断

请注意,在这个例子中,我们没有必要指定类型。类型是由TypeScript推断出来的--自动选择。我们也可以使用多个类型参数。例如,一个独立版本的map看起来是这样的。

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output):
    Output[] {
    return arr.map(func);
}

// 参数'n'是'字符串'类型。
// 'parsed'是'number[]'类型。
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

请注意,在这个例子中,TypeScript可以推断出输入类型参数的类型(从给定的字符串数组),以及基于函数表达式的返回值(数字)的输出类型参数。

限制条件

我们已经写了一些通用函数,可以对任何类型的值进行操作。有时我们想把两个值联系起来,但只能对某个值的子集进行操作。在这种情况下,我们可以使用一个约束条件来限制一个类型参数可以接受的类型。

让我们写一个函数,返回两个值中较长的值。要做到这一点,我们需要一个长度属性,是一个数字。我们通过写一个扩展子句将类型参数限制在这个类型上。

function longest<Type extends { length: number }>(a: Type, b: Type) {
    if (a.length >= b.length) {
        return a;
    } else {
        return b;
    }
}
// longerArray 的类型是 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 是 'alice'|'bob' 的类型。
const longerString = longest("alice", "bob");
// 错误! 数字没有'长度'属性
const notOK = longest(10, 100);

在这个例子中,有一些有趣的事情需要注意。我们允许TypeScript推断 longest 的返回类型。返回类型推断也适用于通用函数。

因为我们将 Type 约束为 { length: number } ,所以我们被允许访问 a 和 b 参数的 .length 属性。如果没有类型约束,我们就不能访问这些属性,因为这些值可能是一些没有长度属性的其他类型。

longerArray 和 longerString 的类型是根据参数推断出来的。记住,泛型就是把两个或多个具有相同类型的值联系起来。

最后,正如我们所希望的,对 longest(10, 100) 的调用被拒绝了,因为数字类型没有一个 .length属性。

使用受限值

这里有一个使用通用约束条件时的常见错误。

function minimumLength<Type extends { length: number }>(
    obj: Type,
    minimum: number
): Type {
    if (obj.length >= minimum) {
        return obj
    } else {
        return { length: minimum }
    }
}

看起来这个函数没有问题--Type被限制为{ length: number },而且这个函数要么返回Type,要么返回一个与该限制相匹配的值。问题是,该函数承诺返回与传入的对象相同的类型,而不仅仅是与约束条件相匹配的一些对象。如果这段代码是合法的,你可以写出肯定无法工作的代码。

// 'arr' 获得值: { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
//在此崩溃,因为数组有一个'切片'方法,但没有返回对象!
console.log(arr.slice(0));

指定类型参数

TypeScript 通常可以推断出通用调用中的预期类型参数,但并非总是如此。例如,假设你写了一个函数来合并两个数组:

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
    return arr1.concat(arr2);
}

通常情况下,用不匹配的数组调用这个函数是一个错误:

const arr = combine([1, 2, 3], ["hello"]);

然而,如果你打算这样做,你可以手动指定类型:

const arr = combine<string | number>([1, 2, 3], ["hello"]);

编写优秀通用函数的准则

编写泛型函数很有趣,而且很容易被类型参数所迷惑。有太多的类型参数或在不需要的地方使用约束,会使推理不那么成功,使你的函数的调用者感到沮丧。

  • 类型参数下推

下面是两种看似相似的函数写法。

function firstElement1<Type>(arr: Type[]) {
    return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
    return arr[0];
}
// a: number (推荐)
const a = firstElement1([1, 2, 3]);
// b: any (不推荐)
const b = firstElement2([1, 2, 3]);

乍一看,这些可能是相同的,但 firstElement1 是写这个函数的一个更好的方法。它的推断返回类型是Type,但 firstElement2 的推断返回类型是 any ,因为TypeScript必须使用约束类型来解析arr[0] 表达式,而不是在调用期间 "等待 "解析该元素。

规则:在可能的情况下,使用类型参数本身,而不是对其进行约束

  • 使用更少的类型参数

下面是另一对类似的函数。

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
    return arr.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
    arr: Type[],
    func: Func
): Type[] {
    return arr.filter(func);
}

我们已经创建了一个类型参数 Func ,它并不涉及两个值。这总是一个值得标记的坏习惯,因为它意味着想要指定类型参数的调用者必须无缘无故地手动指定一个额外的类型参数。 Func 除了使函数更难阅读和推理外,什么也没做。

规则:总是尽可能少地使用类型参数

  • 类型参数应出现两次

有时我们会忘记,一个函数可能不需要是通用的:

function greet<Str extends string>(s: Str) {
    console.log("Hello, " + s);
}
greet("world");

我们完全可以写一个更简单的版本:

function greet(s: string) {
    console.log("Hello, " + s);
}

记住,类型参数是用来关联多个值的类型的。如果一个类型参数在函数签名中只使用一次,那么它就没有任何关系。

规则:如果一个类型的参数只出现在一个地方,请重新考虑你是否真的需要它

可选参数

JavaScript中的函数经常需要一个可变数量的参数。例如, number 的 toFixed 方法需要一个可选的数字计数。

function f(n: number) {
    console.log(n.toFixed()); // 0 个参数
    console.log(n.toFixed(3)); // 1 个参数
}

我们可以在TypeScript中通过将参数用 ? 标记:

function f(x?: number) {
    // ...
}
f(); // 正确
f(10); // 正确

虽然参数被指定为 number 类型,但 x 参数实际上将具有 number | undefined 类型,因为在JavaScript中未指定的参数会得到 undefined 的值。你也可以提供一个参数默认值。

function f(x = 10) {
    // ...
}

现在在 f 的主体中, x 将具有 number 类型,因为任何 undefined 的参数将被替换为 10 。请注意,当一个参数是可选的,调用者总是可以传递未定义的参数,因为这只是模拟一个 "丢失 "的参数:

declare function f(x?: number): void;
// 以下调用都是正确的
f();
f(10);
f(undefined);

回调中的可选参数

一旦你了解了可选参数和函数类型表达式,在编写调用回调的函数时就很容易犯以下错误:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
    for (let i = 0; i < arr.length; i++) {
        callback(arr[i], i);
    }
}

我们在写 index? 作为一个可选参数时,通常是想让这些调用都是合法的:

myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

这实际上意味着回调可能会被调用,只有一个参数。换句话说,该函数定义说,实现可能是这样的:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
    for (let i = 0; i < arr.length; i++) {
        // 我现在不想提供索引
        callback(arr[i]);
    }
}

反过来,TypeScript会强制执行这个意思,并发出实际上不可能的错误:

myForEach([1, 2, 3], (a, i) => {
    console.log(i.toFixed())
})

在JavaScript中,如果你调用一个形参多于实参的函数,额外的参数会被简单地忽略。TypeScript的行为 也是如此。参数较少的函数(相同的类型)总是可以取代参数较多的函数的位置。

当为回调写一个函数类型时,永远不要写一个可选参数,除非你打算在不传递该参数的情况下调用函数。

函数重载

一些 JavaScript 函数可以在不同的参数数量和类型中被调用。例如,你可能会写一个函数来产生一个Date,它需要一个时间戳(一个参数)或一个月/日/年规格(三个参数)。

在TypeScript中,我们可以通过编写重载签名来指定一个可以以不同方式调用的函数。要做到这一点,要写一些数量的函数签名(通常是两个或更多),然后是函数的主体:

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
    if (d !== undefined && y !== undefined) {
        return new Date(y, mOrTimestamp, d);
    } else {
        return new Date(mOrTimestamp);
    }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);

在这个例子中,我们写了两个重载:一个接受一个参数,另一个接受三个参数。这前两个签名被称为重载签名。

然后,我们写了一个具有兼容签名的函数实现。函数有一个实现签名,但这个签名不能被直接调用。即使我们写了一个在所需参数之后有两个可选参数的函数,它也不能以两个参数被调用!

重载签名和实现签名

这是一个常见的混乱来源。通常我们会写这样的代码,却不明白为什么会出现错误:

function fn(x: string): void;
function fn() {
    // ...
}
// 期望能够以零参数调用
fn();

同样,用于编写函数体的签名不能从外面 "看到"。

实现的签名从外面是看不到的。在编写重载函数时,你应该总是在函数的实现上面有两个或多个签名。

实现签名也必须与重载签名兼容。例如,这些函数有错误,因为实现签名没有以正确的方式匹配重载:

function fn(x: boolean): void;
// 参数类型不正确
function fn(x: string): void;
function fn(x: boolean) {}
function fn(x: string): string;
// 返回类型不正确
function fn(x: number): boolean;
function fn(x: string | number) {
    return "oops";
}

如何编写好的函数重载

和泛型一样,在使用函数重载时,有一些准则是你应该遵循的。遵循这些原则将使你的函数更容易调用,更容易理解,更容易实现。

让我们考虑一个返回字符串或数组长度的函数:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
    return x.length;
}

这个函数是好的;我们可以用字符串或数组来调用它。然而,我们不能用一个可能是字符串或数组的值来调用它,因为TypeScript只能将一个函数调用解析为一个重载:

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); // 没有与此调用匹配的重载

因为两个重载都有相同的参数数量和相同的返回类型,我们可以改写一个非重载版本的函数:

function len(x: any[] | string) {
    return x.length;
}
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); // OK

这就好得多了! 调用者可以用任何一种值来调用它,而且作为额外的奖励,我们不需要找出一个正确的实现签名。

在可能的情况下,总是倾向于使用联合类型的参数而不是重载参数

函数内this的声明

TypeScript会通过代码流分析来推断函数中的 this 应该是什么,比如下面的例子:

const user = {
    id: 123,
    admin: false,
    becomeAdmin: function () {
        this.admin = true;
    }
}

TypeScript理解函数 user.becomeAdmin 有一个对应的 this ,它是外部对象 user 。这个对于很多情况来说已经足够了,但是有很多情况下你需要更多的控制 this 代表什么对象。JavaScript规范规定,你不能有一个叫 this 的参数,所以TypeScript使用这个语法空间,让你在函数体中声明 this 的类型。

interface User {
    admin: boolean
}
interface DB {
    filterUsers(filter: (this: User) => boolean): User[];
}
const db:DB = {
    filterUsers: (filter: (this: User) => boolean) => {
        let user1 = {
            admin: true
        }
        let user2 = {
            admin: false
        }
        return [user1, user2]
    }
}
const admins = db.filterUsers(function (this: User) {
    return this.admin;
})

这种模式在回调式API中很常见,另一个对象通常控制你的函数何时被调用。注意,你需要使用函数而不是箭头函数来获得这种行为。

interface User {
    admin: boolean
}
interface DB {
    filterUsers(filter: (this: User) => boolean): User[];
}
const db:DB = {
    filterUsers: (filter: (this: User) => boolean) => {
        let user1 = {
            admin: true
        }
        let user2 = {
            admin: false
        }
        return [user1, user2]
    }
}
// 不能为箭头函数
const admins = db.filterUsers(() => this.admin);

需要了解的其他类型

有一些额外的类型你会想要认识,它们在处理函数类型时经常出现。像所有的类型一样,你可以在任何地方使用它们,但这些类型在函数的上下文中特别相关。

void

void 表示没有返回值的函数的返回值。当一个函数没有任何返回语句,或者没有从这些返回语句中返回任何明确的值时,它都是推断出来的类型。

// 推断出的返回类型是void
function noop() {
    return;
}

在JavaScript中,一个不返回任何值的函数将隐含地返回 undefinded 的值。然而,在TypeScript中,void 和 undefined 是不一样的。

void 与 undefined 不一样。

object

特殊类型 object 指的是任何不是基元的值( string 、 number 、 bigint 、 boolean 、 symbol 、null 或 undefined )。这与空对象类型 {} 不同,也与全局类型 Object 不同。你很可能永远不会使用 Object 。

object 不是 Object 。始终使用 object !

请注意,在JavaScript中,函数值是对象。它们有属性,在它们的原型链中有 Object.prototype ,是Object 的实例,你可以对它们调用 Object.key ,等等。由于这个原因,函数类型在TypeScript中被认为是 object 。

unknown

unknown 类型代表任何值。这与 any 类型类似,但更安全,因为对未知 unknown 值做任何事情都是不合法的。

function f1(a: any) {
    a.b(); // 正确
}
function f2(a: unknown) {
    a.b(); // 报错
}

这在描述函数类型时很有用,因为你可以描述接受任何值的函数,而不需要在函数体中有 any 值。

反之,你可以描述一个返回未知类型的值的函数:

function safeParse(s: string): unknown {
    return JSON.parse(s);
}
// 需要小心对待'obj'!
const obj = safeParse(someRandomString);

never

有些函数永远不会返回一个值:

function fail(msg: string): never {
    throw new Error(msg);
}

never 类型表示永远不会被观察到的值。在一个返回类型中,这意味着函数抛出一个异常或终止程序的执行。

never 也出现在TypeScript确定一个 union 中没有任何东西的时候。

function fn(x: string | number) {
    if (typeof x === "string") {
        // 做一些事
    } else if (typeof x === "number") {
        // 再做一些事
    } else {
        x; // 'never'!
    }
}

Function

全局性的 Function 类型描述了诸如 bind 、 call 、 apply 和其他存在于JavaScript中所有函数值的属性。它还有一个特殊的属性,即 Function 类型的值总是可以被调用;这些调用返回 any 。

function doSomething(f: Function) {
    return f(1, 2, 3);
}

这是一个无类型的函数调用,一般来说最好避免,因为 any 返回类型都不安全。

如果你需要接受一个任意的函数,但不打算调用它,一般来说, () => void 的类型比较安全。

参数展开运算符

形参展开(Rest Parameters)

除了使用可选参数或重载来制作可以接受各种固定参数数量的函数之外,我们还可以使用休止参数来定义接受无限制数量的参数的函数。

rest 参数出现在所有其他参数之后,并使用 ... 的语法:

function multiply(n: number, ...m: number[]) {
    return m.map((x) => n * x);
}
// 'a' 获得的值 [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

在TypeScript中,这些参数的类型注解是隐含的 any[],而不是 any,任何给出的类型注解必须是Array<T>或T[]的形式,或一个元组类型。

实参展开(Rest Arguments)

反之,我们可以使用 spread 语法从数组中提供可变数量的参数。例如,数组的 push 方法需要任意数量的参数。

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

请注意,一般来说,TypeScript并不假定数组是不可变的。这可能会导致一些令人惊讶的行为。

// 推断的类型是 number[] -- "一个有零或多个数字的数组"。
// 不专指两个数字
const args = [8, 5];
const angle = Math.atan2(...args);

这种情况的最佳解决方案取决于你的代码,但一般来说, const context 是最直接的解决方案。

// 推断为2个长度的元组
const args = [8, 5] as const;
// 正确
const angle = Math.atan2(...args);

参数解构

你可以使用参数重构来方便地将作为参数提供的对象,解压到函数主体的一个或多个局部变量中。在 JavaScript中,它看起来像这样:

function sum({ a, b, c }) {
    console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });

对象的类型注解在解构的语法之后:

function sum({ a, b, c }: { a: number; b: number; c: number }) {
    console.log(a + b + c);
}

这看起来有点啰嗦,但你也可以在这里使用一个命名的类型:

// 与之前的例子相同
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
    console.log(a + b + c);
}

函数的可分配性

返回void类型

函数的 void 返回类型可以产生一些不寻常的,但却是预期的行为。

返回类型为 void 的上下文类型并不强迫函数不返回东西。另一种说法是,一个具有 void 返回类型的上下文函数类型(type vf = () => void),在实现时,可以返回任何其他的值,但它会被忽略。

因此,以下 () => void 类型的实现是有效的:

type voidFunc = () => void
    const f1: voidFunc = () => {
    return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
    return true
}

而当这些函数之一的返回值被分配给另一个变量时,它将保留 void 的类型:

const v1 = f1();
const v2 = f2();
const v3 = f3();

这种行为的存在使得下面的代码是有效的,即使 Array.prototype.push 返回一个数字,而 Array.prototype.forEach 方法期望一个返回类型为 void 的函数:

const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));

还有一个需要注意的特殊情况,当一个字面的函数定义有一个 void 的返回类型时,该函数必须不返回任何东西。

function f2(): void {
    return true; // 报错:不能将类型“boolean”分配给类型“void”
}
const f3 = function (): void {
    return true; // 报错:不能将类型“boolean”分配给类型“void”
};

对象类型

在JavaScript中,我们分组和传递数据的基本方式是通过对象。在TypeScript中,我们通过对象类型来表示这些对象。

正如我们所见,它们可以是匿名的:

function greet(person: { name: string; age: number }) {
    return "Hello " + person.name;
}

或者可以通过使用一个接口来命名它们:

interface Person {
    name: string;
    age: number;
}
function greet(person: Person) {
    return "Hello " + person.name;
}

或一个类型别名:

type Person = {
    name: string;
    age: number;
};
function greet(person: Person) {
    return "Hello " + person.name;
}

在上面的三个例子中,我们写了一些函数,这些函数接收包含属性 name (必须是一个 string )和age(必须是一个 number )的对象。

属性修改器

对象类型中的每个属性都可以指定几件事:类型、属性是否是可选的,以及属性是否可以被写入。

可选属性

很多时候,我们会发现自己处理的对象可能有一个属性设置。在这些情况下,我们可以在这些属性的名字后面加上一个问号(?),把它们标记为可选的。

type Shape = {}
interface PaintOptions {
    shape: Shape;
    xPos?: number;
    yPos?: number;
}
function paintShape(opts: PaintOptions) {
    // ...
}
const shape:Shape = {}
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

在这个例子中, xPos 和 yPos 都被认为是可选的。我们可以选择提供它们中的任何一个,所以上面对paintShape 的每个调用都是有效的。所有的可选性实际上是说,如果属性被设置,它最好有一个特定的类型。

我们也可以从这些属性中读取,但当我们在 strictNullChecks 下读取时,TypeScript会告诉我们它们可能是未定义的。

function paintShape(opts: PaintOptions) {
    let xPos = opts.xPos;
    let yPos = opts.yPos;
    // ...
}

在JavaScript中,即使该属性从未被设置过,我们仍然可以访问它--它只是会给我们未定义的值。我们可以专门处理未定义。

function paintShape(opts: PaintOptions) {
    let xPos = opts.xPos === undefined ? 0 : opts.xPos;
    let yPos = opts.yPos === undefined ? 0 : opts.yPos;
    // ...
}

请注意,这种为未指定的值设置默认值的模式非常普遍,以至于JavaScript有语法来支持它。

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
    console.log("x coordinate at", xPos);
    console.log("y coordinate at", yPos);
    // ...
}

在这里,我们为 paintShape 的参数使用了一个解构模式,并为 xPos 和 yPos 提供了默认值。现在xPos 和 yPos 都肯定存在于 paintShape 的主体中,但对于 paintShape 的任何调用者来说是可选的。

请注意,目前还没有办法将类型注释放在解构模式中。这是因为下面的语法在JavaScript中已经有了不同的含义。

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
    render(shape);
    render(xPos); // 报错:绑定元素Shape隐式具有“any”类型
}

在一个对象解构模式中, shape: Shape 意味着 "获取属性 shape ,并在本地重新定义为一个名为 Shape 的变量。同样, xPos: number 创建一个名为number的变量,其值基于参数的 xPos 。

只读属性

对于TypeScript,属性也可以被标记为只读。虽然它不会在运行时改变任何行为,但在类型检查期间,一个标记为只读的属性不能被写入。

interface SomeType {
    readonly prop: string;
}
function doSomething(obj: SomeType) {
    // 可以读取 'obj.prop'.
    console.log(`prop has the value '${obj.prop}'.`);
    // 但不能重新设置值
    obj.prop = "hello";
}

使用 readonly 修饰符并不一定意味着一个值是完全不可改变的。或者换句话说,它的内部内容不能被改变,它只是意味着该属性本身不能被重新写入。

interface Home {
    readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
    // 我们可以从'home.resident'读取和更新属性。
    console.log(`Happy birthday ${home.resident.name}!`);
    home.resident.age++;
}
function evict(home: Home) {
    // 但是我们不能写到'home'上的'resident'属性本身。
    home.resident = {
    name: "Victor the Evictor",
    age: 42,
    };
}

管理对 readonly 含义的预期是很重要的。在TypeScript的开发过程中,对于一个对象应该如何被使用的问题,它是有用的信号。TypeScript在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是否是 readonly ,所以 readony 属性也可以通过别名来改变。

interface Person {
    name: string;
    age: number;
}
interface ReadonlyPerson {
    readonly name: string;
    readonly age: number;
}
let writablePerson: Person = {
    name: "Person McPersonface",
    age: 42,
};
// 正常工作
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // 打印 '42'
writablePerson.age++;
console.log(readonlyPerson.age); // 打印 '43'

索引签名

有时你并不提前知道一个类型的所有属性名称,但你知道值的形状。

在这些情况下,你可以使用一个索引签名来描述可能的值的类型,比如说:

interface StringArray {
    [index: number]: string;
}
const myArray: StringArray = ['a', 'b'];
const secondItem = myArray[1];

上面,我们有一个 StringArray 接口,它有一个索引签名。这个索引签名指出,当一个 StringArray 被数字索引时,它将返回一个字符串。

索引签名的属性类型必须是 string 或 number 。

支持两种类型的索引器是可能的,但是从数字索引器返回的类型必须是字符串索引器返回的类型的子类型。这是因为当用 "数字 "进行索引时,JavaScript实际上会在索引到一个对象之前将其转换为 "字符串"。这意味着用 100 (一个 数字 )进行索引和用 "100" (一个 字符串 )进行索引是一样的,所以两者需要一致。

interface Animal {
    name: string;
}
interface Dog extends Animal {
    breed: string;
}
interface NotOkay {
    [x: number]: Animal; // 报错:“number”索引类型“Animal”不能分配给“string”索引类型“Dog”
    [x: string]: Dog;
}

虽然字符串索引签名是描述 "字典 "模式的一种强大方式,但它也强制要求所有的属性与它们的返回类型相匹配。这是因为字符串索引声明 obj.property 也可以作为 obj["property"] 。在下面的例子中,name 的类型与字符串索引的类型不匹配,类型检查器会给出一个错误:

interface NumberDictionary {
    [index: string]: number;
    length: number; // ok
    name: string; // 报错:“string”的属性“name”不能赋值给“string”索引类型“number”
}

然而,如果索引签名是属性类型的联合,不同类型的属性是可以接受的:

interface NumberOrStringDictionary {
    [index: string]: number | string;
    length: number; // 正确, length 是 number 类型
    name: string; // 正确, name 是 string 类型
}

最后,你可以使索引签名为只读,以防止对其索引的赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory"; // 报错:类型“ReadonlyStringArray”中的索引签名仅允许读取。

你不能设置 myArray[2] ,因为这个索引签名是只读的。

扩展类型

有一些类型可能是其他类型的更具体的版本,这是很常见的。例如,我们可能有一个 BasicAddress 类型,描述发送信件和包裹所需的字段。

interface BasicAddress {
    name?: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}

在某些情况下,这就足够了,但是如果一个地址的小区内有多个单元,那么地址往往有一个单元号与之相关。我们就可以描述一个 AddressWithUnit :

interface AddressWithUnit {
    name?: string;
    unit: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}

这就完成了工作,但这里的缺点是,当我们的变化是纯粹的加法时,我们不得不重复 BasicAddress 的所有其他字段。相反,我们可以扩展原始的 BasicAddress 类型,只需添加 AddressWithUnit 特有的新字段:

interface BasicAddress {
    name?: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
    unit: string;
}

接口上的 extends 关键字,允许我们有效地从其他命名的类型中复制成员,并添加我们想要的任何新成员。这对于减少我们不得不写的类型声明模板,以及表明同一属性的几个不同声明可能是相关的意图来说,是非常有用的。例如, AddressWithUnit 不需要重复 street 属性,而且因为 street 源于BasicAddress ,我们会知道这两种类型在某种程度上是相关的。

接口也可以从多个类型中扩展。

interface Colorful {
    color: string;
}
interface Circle {
    radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
    const cc: ColorfulCircle = {
    color: "red",
    radius: 42,
};

交叉类型

接口允许我们通过扩展其他类型建立起新的类型。TypeScript提供了另一种结构,称为交叉类型,主要用于组合现有的对象类型。

交叉类型是用 & 操作符定义的。

interface Colorful {
    color: string;
}
interface Circle {
    radius: number;
}
type ColorfulCircle = Colorful & Circle;
const cc: ColorfulCircle = {
    color: "red",
    radius: 42,
}

在这里,我们将 Colorful 和 Circle 相交,产生了一个新的类型,它拥有 Colorful 和 Circle 的所有成员。

function draw(circle: Colorful & Circle) {
    console.log(`Color was ${circle.color}`);
    console.log(`Radius was ${circle.radius}`);
}
// 正确
draw({ color: "blue", radius: 42 });
// 错误
draw({ color: "red", raidus: 42 });

接口与交叉类型

我们刚刚看了两种组合类型的方法,它们很相似,但实际上有细微的不同。对于接口,我们可以使用extends子句来扩展其他类型,而对于交叉类型,我们也可以做类似的事情,并用类型别名来命名结果。两者之间的主要区别在于如何处理冲突,这种区别通常是你在接口和交叉类型的类型别名之间选择一个的主要原因之一。

Alt text转存失败,建议直接上传图片文件

接口可以定义多次,多次的声明会自动合并:

interface Sister {
    name: string;
}
interface Sister {
    age: number;
}
const sisterAn: Sister = {
    name: 'sisterAn'
}
const sisterRan: Sister = {
    name: 'sisterRan',
    age: 12
}

但是类型别名如果定义多次,会报错:

type Sister = {
    name: string;
}

// 报错:标识符“Sister”重复
type Sister = {
    age: number;
}

泛型对象类型

让我们想象一下,一个可以包含任何数值的盒子类型:字符串、数字、长颈鹿,等等。

interface Box {
    contents: any;
}

现在,内容属性的类型是任意,这很有效,但会导致下一步的意外。

我们可以使用 unknown ,但这意味着在我们已经知道内容类型的情况下,我们需要做预防性检查,或者使用容易出错的类型断言。

interface Box {
    contents: unknown;
}
let x: Box = {
    contents: "hello world",
};
// 我们需要检查 'x.contents'
if (typeof x.contents === "string") {
    console.log(x.contents.toLowerCase());
}
// 或者用类型断言
console.log((x.contents as string).toLowerCase());

一种安全的方法是为每一种类型的内容搭建不同的盒子类型:

interface NumberBox {
    contents: number;
}
interface StringBox {
    contents: string;
}
interface BooleanBox {
    contents: boolean;
}

但这意味着我们必须创建不同的函数,或函数的重载,以对这些类型进行操作:

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
    box.contents = newContents;
}

那是一个很大的模板。此外,我们以后可能需要引入新的类型和重载。这是令人沮丧的,因为我们的盒子类型和重载实际上都是一样的。

相反,我们可以做一个通用的 Box 类型,声明一个类型参数:

interface Box<Type> {
    contents: Type;
}

你可以把这句话理解为:"一个类型的盒子,是它的内容具有类型的东西"。以后,当我们引用 Box 时,我们必须给一个类型参数来代替 Type 。

let box: Box<string>;

把 Box 想象成一个真实类型的模板,其中 Type 是一个占位符,会被替换成其他类型。当 TypeScript看到Box<string> 时,它将用字符串替换 Box<Type> 中的每个 Type 实例,并最终以 { contents:string } 这样的方式工作。换句话说, Box<string> 和我们之前的 StringBox 工作起来是一样的。

interface Box<Type> {
    contents: Type;
}
interface StringBox {
    contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
let boxB: StringBox = { contents: "world" };
boxB.contents;

盒子是可重用的,因为Type可以用任何东西来代替。这意味着当我们需要一个新类型的盒子时,我们根本不需要声明一个新的盒子类型(尽管如果我们想的话,我们当然可以)。

interface Box<Type> {
    contents: Type;
}
interface Apple {
    // ....
}
// 等价于 '{ contents: Apple }'.
type AppleBox = Box<Apple>;

这也意味着我们可以完全避免重载,而是使用通用函数。

function setContents<Type>(box: Box<Type>, newContents: Type) {
    box.contents = newContents;
}

值得注意的是,类型别名也可以是通用的。我们可以定义我们新的 Box 接口:

interface Box<Type> {
    contents: Type;
}

通过使用一个类型别名来代替:

type Box<Type> = {
    contents: Type;
}

由于类型别名与接口不同,它不仅可以描述对象类型,我们还可以用它来编写其他类型的通用辅助类型。

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

我们将在稍后回到类型别名。

通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种方式工作是很理想的,这样它们就可以在不同的数据类型中重复使用。

数组类型

我们一直在使用这样一种类型:数组类型。每当我们写出 number[] 或 string[] 这样的类型时,这实际上只是 Array 和 Array 的缩写。

function doSomething(value: Array<string>) {
    // ...
}
let myArray: string[] = ["hello", "world"];
// 这两样都能用
doSomething(myArray);
doSomething(new Array("hello", "world"));

和上面的 Box 类型一样, Array 本身也是一个通用类型。

interface Array<Type> {
/**
* 获取或设置数组的长度。
*/
length: number;
/**
* 移除数组中的最后一个元素并返回。
*/
pop(): Type | undefined;
/**
* 向一个数组添加新元素,并返回数组的新长度。
*/
push(...items: Type[]): number;
    // ...
}

现代JavaScript还提供了其他通用的数据结构,比如 Map<K, V> , Set<T> , Promise<T> 。这实际上意味着,由于 Map 、 Set 和 Promise 的行为方式,它们可以与任何类型的集合一起工作。

只读元组类型

关于 tuple 类型的最后一点说明: tuple 类型有只读特性,可以通过在它们前面粘贴一个readonly修饰符来指定——就像数组的速记语法一样。

function doSomething(pair: readonly [string, number]) {
    // ...
}

正如你所期望的,在TypeScript中不允许向只读元组的任何属性写入。

function doSomething(pair: readonly [string, number]) {
    pair[0] = "hello!";
}

在大多数代码中,元组往往被创建并不被修改,所以在可能的情况下,将类型注释为只读元组是一个很好的默认。这一点也很重要,因为带有const断言的数组字面量将被推断为只读元组类型。

let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
    return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);

在这里, distanceFromOrigin 从未修改过它的元素,而是期望一个可变的元组。由于 point 的类型被推断为只读的 [3, 4] ,它与 [number, number] 不兼容,因为该类型不能保证 point 的元素不被修改。

类型操纵

从类型中创建类型

TypeScript的类型系统非常强大,因为它允许用其他类型的术语来表达类型。

这个想法的最简单的形式是泛型,我们实际上有各种各样的类型操作符可以使用。也可以用我们已经有的值来表达类型。

通过结合各种类型操作符,我们可以用一种简洁、可维护的方式来表达复杂的操作和值。在本节中,我们将介绍用现有的类型或值来表达一个新类型的方法。

  • 泛型型 - 带参数的类型
  • Keyof 类型操作符- keyof 操作符创建新类型
  • Typeof 类型操作符 - 使用 typeof 操作符来创建新的类型`
  • 索引访问类型 - 使用 Type['a'] 语法来访问一个类型的子集
  • 条件类型 - 在类型系统中像if语句一样行事的类型
  • 映射类型 - 通过映射现有类型中的每个属性来创建类型
  • 模板字面量类型 - 通过模板字面字符串改变属性的映射类型

泛型

软件工程的一个主要部分是建立组件,这些组件不仅有定义明确和一致的API,而且还可以重复使用。能够处理今天的数据和明天的数据的组件将为你建立大型软件系统提供最灵活的能力。

在像C#和Java这样的语言中,创建可重用组件的工具箱中的主要工具之一是泛型,也就是说,能够创建一个在各种类型上工作的组件,而不是单一的类型。这使得用户可以消费这些组件并使用他们自己的类型。

Hello World

首先,让我们做一下泛型的 " hello world":身份函数。身份函数是一个函数,它将返回传入的任何内容。你可以用类似于echo命令的方式来考虑它。

如果没有泛型,我们将不得不给身份函数一个特定的类型。

function identity(arg: number): number {
    return arg;
}

或者,我们可以用任意类型来描述身份函数。

function identity(arg: any): any {
    return arg;
}

使用 any 当然是通用的,因为它将使函数接受 arg 类型的任何和所有的类型,但实际上我们在函数返回时失去了关于该类型的信息。如果我们传入一个数字,我们唯一的信息就是任何类型都可以被返回。

相反,我们需要一种方法来捕获参数的类型,以便我们也可以用它来表示返回的内容。在这里,我们将使用一个类型变量,这是一种特殊的变量,对类型而不是数值起作用。

function identity<Type>(arg: Type): Type {
    return arg;
}

我们现在已经在身份函数中添加了一个类型变量 Type 。这个 Type 允许我们捕获用户提供的类型(例如数字),这样我们就可以在以后使用这些信息。这里,我们再次使用Type作为返回类型。经过检查,我们现在可以看到参数和返回类型使用的是相同的类型。这使得我们可以将类型信息从函数的一侧输入,然后从另一侧输出。

我们说这个版本的身份函数是通用的,因为它在一系列的类型上工作。与使用任何类型不同的是,它也和第一个使用数字作为参数和返回类型的身份函数一样精确(即,它不会丢失任何信息)。

一旦我们写好了通用身份函数,我们就可以用两种方式之一来调用它。第一种方式是将所有的参数,包括类型参数,都传递给函数:

let output = identity<string>("myString");

这里我们明确地将 Type 设置为 string ,作为函数调用的参数之一,用参数周围的 <> 而不是 () 来表示。

第二种方式可能也是最常见的。这里我们使用类型参数推理——也就是说,我们希望编译器根据我们传入的参数的类型,自动为我们设置 Type 的值。

let output = identity("myString");

注意,我们不必在角括号(<>)中明确地传递类型;编译器只是查看了 "myString "这个值,并将Type设置为其类型。虽然类型参数推断是一个有用的工具,可以使代码更短、更易读,但当编译器不能推断出类型时,你可能需要像我们在前面的例子中那样明确地传入类型参数,这在更复杂的例子中可能发生。

使用通用类型变量

当你开始使用泛型时,你会注意到,当你创建像 identity 这样的泛型函数时,编译器会强制要求你在函数主体中正确使用任何泛型参数。也就是说,你实际上是把这些参数当作是任何和所有的类型。

让我们来看看我们前面的 identity 函数。

function identity<Type>(arg: Type): Type {
    return arg;
}

如果我们想在每次调用时将参数 arg 的长度记录到控制台,该怎么办?我们可能很想这样写:

function loggingIdentity<Type>(arg: Type): Type {
    console.log(arg.length); // 报错:类型“Type”上不存在属性“length”
    return arg;
}

当我们这样做时,编译器会给我们一个错误,说我们在使用 arg.length 成员,但我们没有说any 有这个成员。记住,我们在前面说过,这些类型的变量可以代表任何和所有的类型,所以使用这个函数的人可以传入一个 number ,而这个数字没有一个 .length 成员。比方说,我们实际上是想让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。既然我们在处理数组,那么 .length 成员应该是可用的。我们可以像创建其他类型的数组那样来描述它。

function loggingIdentity<Type>(arg: Type[]): Type[] {
    console.log(arg.length);
    return arg;
}

你可以把 loggingIdentity 的类型理解为 "通用函数 loggingIdentity 接收一个类型参数 Type 和一个参数 arg , arg 是一个 Type 数组,并返回一个 Type 数组。" 如果我们传入一个数字数组,我们会得到一个数字数组,因为Type会绑定到数字。这允许我们使用我们的通用类型变量 Type 作为我们正在处理的类型的一部分,而不是整个类型,给我们更大的灵活性。

我们也可以这样来写这个例子:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
    console.log(arg.length); // 数组有一个.length,所以不会再出错了
    return arg;
}

你可能已经从其他语言中熟悉了这种类型的风格。在下一节中,我们将介绍如何创建你自己的通用类型,如 Array<Type>

泛型类型

在前几节中,我们创建了在一系列类型上工作的通用身份函数。在这一节中,我们将探讨函数本身的类型以及如何创建通用接口。

泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:

function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;

我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致。

function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;

我们也可以把泛型写成一个对象字面类型的调用签名。

function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;

这让我们开始编写我们的第一个泛型接口。让我们把前面例子中的对象字面意思移到一个接口中。

interface GenericIdentityFn {
    <Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;

在一个类似的例子中,我们可能想把通用参数移到整个接口的参数上。这可以让我们看到我们的泛型是什么类型(例如, Dictionary<string> 而不是仅仅 Dictionary )。这使得类型参数对接口的所有其他成员可见。

interface GenericIdentityFn<Type> {
    (arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

请注意,我们的例子已经改变了,变成了稍微不同的东西。我们现在没有描述一个泛型函数,而是有一个非泛型的函数签名,它是泛型类型的一部分。当我们使用 GenericIdentityFn 时,我们现在还需要指定相应的类型参数(这里是:数字),有效地锁定了底层调用签名将使用什么。了解什么时候把类型参数直接放在调用签名上,什么时候把它放在接口本身,将有助于描述一个类型的哪些方面是通用的。

除了泛型接口之外,我们还可以创建泛型类。注意,不可能创建泛型枚举和命名空间。

泛型类

一个泛型类的形状与泛型接口相似。泛型类在类的名字后面有一个角括号(<>)中的泛型参数列表。

class GenericNumber<NumType> {
    zeroValue: NumType;
    add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
    return x + y;
};

这是对 GenericNumber 类相当直白的使用,但你可能已经注意到,没有任何东西限制它只能使用数字类型。我们本可以使用字符串或更复杂的对象。

let stringNumeric = new GenericNumber<string>();
    stringNumeric.zeroValue = "";
    stringNumeric.add = function (x, y) {
    return x + y;
}
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。

正如我们在关于类的章节中提到的,一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数。

泛型约束

如果你还记得前面的例子,你有时可能想写一个通用函数,在一组类型上工作,而你对这组类型会有什么能力有一定的了解。在我们的 loggingIdentity 例子中,我们希望能够访问 arg.length 属性,但是编译器无法证明每个类型都有一个 .length 属性,所以它警告我们不能做这个假设。

function loggingIdentity<Type>(arg: Type): Type {
    console.log(arg.length); // 报错:类型“Type”上不存在属性“length”
    return arg;
}

我们希望限制这个函数与 any 和所有类型一起工作,而不是与 any 和所有同时具有 .length 属性的类型一起工作。只要这个类型有这个成员,我们就允许它,但它必须至少有这个成员。要做到这一点,我们必须把我们的要求作为一个约束条件列在 Type 可以是什么。

为了做到这一点,我们将创建一个接口来描述我们的约束。在这里,我们将创建一个接口,它有一个单一的 .length 属性,然后我们将使用这个接和 extends 关键字来表示我们的约束条件。

interface Lengthwise {
    length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
    console.log(arg.length); //现在我们知道它有一个.length 属性,所以不再有错误了
    return arg;
}

因为泛型函数现在被限制了,它将不再对 any 和 所有的类型起作用。

相反,我们需要传入其类型具有所有所需属性的值。

loggingIdentity({ length: 10, value: 3 });

在泛型约束中使用类型参数

你可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在于 obj 上的属性,所以我们要在这两种类型之间放置一个约束条件。

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // 报错

在泛型中使用类类型

在TypeScript中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型。比如说:

function create<Type>(c: { new (): Type }): Type {
    return new c();
}

一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系。

class BeeKeeper {
    hasMask: boolean = true;
}
class ZooKeeper {
    nametag: string = "Mikle";
}
class Animal {
    numLegs: number = 4;
}
class Bee extends Animal {
    keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
    keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

Keyof类型操作符

keyof 运算符接收一个对象类型,并产生其键的字符串或数字字面联合。下面的类型P与 "x"|"y "是同一类型。

type Point = { x: number; y: number };
type P = keyof Point;
const p1:P = 'x'
const p2:P = 'y'

如果该类型有一个字符串或数字索引签名, keyof 将返回这些类型。

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
const a:A = 0
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
const m:M = 'a'
const m2:M = 10

注意,在这个例子中, M 是 string|number ——这是因为JavaScript对象的键总是被强制为字符串,所以 obj[0] 总是与 obj["0"] 相同。

Typeof 类型操作符

JavaScript已经有一个 typeof 操作符,你可以在表达式上下文中使用。

// 输出 "string"
console.log(typeof "Hello world");

TypeScript添加了一个 typeof 操作符,你可以在类型上下文中使用它来引用一个变量或属性的类型。

let s = "hello";
let n: typeof s;
n = 'world'
n= 100 // 报错:不能将类型“number”分配给类型“string”

这对基本类型来说不是很有用,但结合其他类型操作符,你可以使用typeof来方便地表达许多模式。举一个例子,让我们先看看预定义的类型 ReturnType<T> 。它接收一个函数类型并产生其返回类型:

type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;

如果我们试图在一个函数名上使用 ReturnType ,我们会看到一个指示性的错误。

function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>; // 报错:“f”表示值,但此处用作类型。是否指类型“f”

请记住,值和类型并不是一回事。为了指代值f的类型,我们使用 typeof

function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;

TypeScript 故意限制了你可以使用 typeof 的表达式种类。

具体来说,只有在标识符(即变量名)或其属性上使用typeof是合法的。这有助于避免混乱的陷阱,即编写你认为是在执行的代码,但其实不是。

// 我们认为使用 = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?"); // 报错

索引访问类型

我们可以使用一个索引访问类型来查询另一个类型上的特定属性:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];

索引类型本身就是一个类型,所以我们可以完全使用 unionskeyof 或者其他类型。

interface Person {
    name: string
    age: number
    alive: boolean
}
// type I1 = string | number
type I1 = Person["age" | "name"];
const i11:I1 = 100
const i12:I1 = ''

// type I2 = string | number | boolean
type I2 = Person[keyof Person];
const i21:I2 = ''
const i22:I2 = 100
const i23:I2 = false

// type I3 = Person[AliveOrName];
type AliveOrName = "alive" | "name";
const aon1:AliveOrName = 'alive'
const aon2:AliveOrName = 'name'

如果你试图索引一个不存在的属性,你甚至会看到一个错误:

type I1 = Person["alve"]; // 报错:类型“Person”上不存在属性“alve”

另一个使用任意类型进行索引的例子是使用 number 来获取一个数组元素的类型。我们可以把它和typeof 结合起来,方便地获取一个数组字面的元素类型。

const MyArray = [
    { name: "Alice", age: 15 },
    { name: "Bob", age: 23 },
    { name: "Eve", age: 38 },
];

/* type Person = {
    name: string;
    age: number;
} */
type Person = typeof MyArray[number];
    const p:Person = {
    name: 'xiaoqian',
    age: 11
}

// type Age = number
type Age = typeof MyArray[number]["age"];
const age:Age = 11
// 或者
// type Age2 = number
type Age2 = Person["age"];
const age2:Age2 = 11

你只能在索引时使用类型,这意味着你不能使用 const 来做一个变量引用:

const key = "age";
type Age = Person[key];  // 报错:类型“any”不能作为索引类型使用
// "key"表示值,但在此处用作类型。是否指类型“key”

然而,你可以使用类型别名来实现类似的重构风格:

type key = "age";
type Age = Person[key];

条件类型

在大多数有用的程序的核心,我们必须根据输入来做决定。JavaScript程序也不例外,但鉴于数值可以很容易地被内省,这些决定也是基于输入的类型。条件类型有助于描述输入和输出的类型之间的关系。

interface Animal {
    live(): void;
}
interface Dog extends Animal {
    woof(): void;
}
// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;

条件类型的形式看起来有点像JavaScript中的条件表达式( condition ? trueExpression :falseExpression )

SomeType extends OtherType ? TrueType : FalseType;

当 extends 左边的类型可以赋值给右边的类型时,那么你将得到第一个分支中的类型("真 "分支);否则你将得到后一个分支中的类型("假 "分支)。

从上面的例子来看,条件类型可能并不立即显得有用——我们可以告诉自己是否 Dog extends Animal ,并选择 numberstring !但条件类型的威力来自于它所带来的好处。条件类型的力量来自于将它们与泛型一起使用。

例如,让我们来看看下面这个 createLabel 函数:

interface IdLabel {
    id: number /* 一些字段 */;
}
interface NameLabel {
    name: string /* 另一些字段 */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}

createLabel 的这些重载描述了一个单一的JavaScript函数,该函数根据其输入的类型做出选择。注意一些事情:

  • 如果一个库必须在其API中反复做出同样的选择,这就会变得很麻烦。
  • 我们必须创建三个重载:一个用于确定类型的情况(一个用于 string ,一个用于 number ),一个用于最一般的情况(取一个 string | number )。对于 createLabel 所能处理的每一种新类型,重载的数量都会呈指数级增长。

相反,我们可以在一个条件类型中对该逻辑进行编码:

type NameOrId<T extends number | string> = T extends number
    ? IdLabel
    : NameLabel;

然后我们可以使用该条件类型,将我们的重载简化为一个没有重载的单一函数。

interface IdLabel {
    id: number /* some fields */;
}
interface NameLabel {
    name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number
    ? IdLabel
    : NameLabel;
    function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
    throw "unimplemented";
}
// let a: NameLabel
let a = createLabel("typescript");
// let b: IdLabel
let b = createLabel(2.8);
// let c: NameLabel | IdLabel
let c = createLabel(Math.random() ? "hello" : 42);

条件类型约束

通常,条件类型中的检查会给我们提供一些新的信息。就像用类型守卫缩小范围可以给我们一个更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步约束泛型。

例如,让我们来看看下面的例子:

type MessageOf<T> = T["message"]; // 报错:类型“message”无法用于类型“T”

在这个例子中,TypeScript出错是因为 T 不知道有一个叫做 message 的属性。我们可以对 T 进行约束,TypeScript就不会再报错。

type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
    message: string;
}
type EmailMessageContents = MessageOf<Email>;

然而,如果我们想让 MessageOf 接受任何类型,并在消息属性不可用的情况下,默认为 never 类型呢?我们可以通过将约束条件移出,并引入一个条件类型来做到这一点。

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
    message: string;
}
interface Dog {
    bark(): void;
}
// type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
const emc:EmailMessageContents = 'balabala...'
// type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
const dmc:DogMessageContents = 'error' as never

在真正的分支中,TypeScript知道 T 会有一个消息属性。

作为另一个例子,我们也可以写一个叫做 Flatten 的类型,将数组类型平铺到它们的元素类型上,但在其他方面则不做处理。

type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;
// 单独一个类型。
// type Num = number
type Num = Flatten<number>;

当 Flatten 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取 string[] 的元素类型。否则,它只是返回它被赋予的类型。

在条件类型内进行推理

我们只是发现自己使用条件类型来应用约束条件,然后提取出类型。这最终成为一种常见的操作,而条件类型使它变得更容易。

条件类型为我们提供了一种方法来推断我们在真实分支中使用 infer 关键字进行对比的类型。例如,我们可以在 Flatten 中推断出元素类型,而不是用索引访问类型 "手动 "提取出来。

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

在这里,我们使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量,而不是指定如何在真实分支中检索 T 的元素类型。这使我们不必考虑如何挖掘和探测我们感兴趣的类型的结构。

我们可以使用 infer 关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数类型中提取出返回类型。

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
    ? Return
    : never;
// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// 给泛型传入 string 类型,条件类型会返回 never
type Never = GetReturnType<string>
const nev:Never = 'error' as never

当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断(据推测,这是最容许的万能情况)。不可能根据参数类型的列表来执行重载解析。

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;

分布式条件类型

当条件类型作用于一个通用类型时,当给定一个联合类型时,它们就变成了分布式的。例如,以下面的例子为例:

type ToArray<Type> = Type extends any ? Type[] : never;

如果我们将一个联合类型插入ToArray,那么条件类型将被应用于该联合的每个成员。

type ToArray<Type> = Type extends any ? Type[] : never;
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

这里发生的情况是,StrArrOrNumArr分布在:

string | number;

并对联合的每个成员类型进行映射,以达到有效的目的:

ToArray<string> | ToArray<number>;

这给我们留下了:

string[] | number[];

通常情况下,分布性是需要的行为。为了避免这种行为,你可以用方括号包围 extends 关键字的每一边。

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr'不再是一个联合类型
// type StrArrOrNumArr = (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;

映射类型

当你不想重复定义类型,一个类型可以以另一个类型为基础创建新类型。

映射类型建立在索引签名的语法上,索引签名用于声明没有被提前声明的属性类型。

type OnlyBoolsAndHorses = {
    [key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
    del: true,
    rodney: false,
};

映射类型是一种通用类型,它使用 PropertyKeys 的联合(经常通过 keyof 创建)迭代键来创建一个类型。

type OptionsFlags<Type> = {
    [Property in keyof Type]: boolean;
};

在这个例子中, OptionsFlags 将从 Type 类型中获取所有属性,并将它们的值改为布尔值。

type FeatureFlags = {
    darkMode: () => void;
    newUserProfile: () => void;
};
/*
type FeatureOptions = {
    darkMode: boolean;
    newUserProfile: boolean;
}
*/
type FeatureOptions = OptionsFlags<FeatureFlags>;

映射修改器

在映射过程中,有两个额外的修饰符可以应用: readonly 和 ? ,它们分别影响可变性和可选性。 你可以通过用 - 或 + 作为前缀来删除或添加这些修饰语。如果你不加前缀,那么就假定是 + 。

type CreateMutable<Type> = {
    // 从一个类型的属性中删除 "readonly"属性
    -readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
    readonly id: string;
    readonly name: string;
};
/*
type UnlockedAccount = {
    id: string;
    name: string;
}
*/
type UnlockedAccount = CreateMutable<LockedAccount>;
// 从一个类型的属性中删除 "可选" 属性
type Concrete<Type> = {
    [Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
    id: string;
    name?: string;
    age?: number;
};
/*
type User = {
    id: string;
    name: string;
    age: number;
}
*/
type User = Concrete<MaybeUser>;

通过 as 做 key 重映射

在TypeScript 4.1及以后的版本中,你可以通过映射类型中的as子句重新映射映射类型中的键。

type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}

你可以利用模板字面类型等功能,从先前的属性名称中创建新的属性名称。

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () =>
    Type[Property]
};
interface Person {
    name: string;
    age: number;
    location: string;
}
/*
type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}
*/
type LazyPerson = Getters<Person>;

你可以通过条件类型产生 never 滤掉的键。

// 删除 "kind"属性
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
/*
type KindlessCircle = {
    radius: number;
}
*/
interface Circle {
    kind: "circle";
    radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;

你可以映射任意的联合体,不仅仅是 string | number | symbol 的联合体,还有任何类型的联合体。

type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
/*
type Config = {
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
}
*/
type Config = EventConfig<SquareEvent | CircleEvent>

进一步探索

映射类型与本类型操作部分的其他功能配合得很好,例如,这里有一个使用条件类型的映射类型 ,它根据一个对象的属性 pii 是否被设置为字面意义上的 true ,返回 true 或 false 。

type ExtractPII<Type> = {
    [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
/*
type ObjectsNeedingGDPRDeletion = {
    id: false;
    name: true;
}
*/
type DBFields = {
    id: { format: "incrementing" };
    name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>

TypeScript提供了对ES2015中引入的 class 关键词的完全支持。

与其他JavaScript语言功能一样,TypeScript增加了类型注释和其他语法,允许你表达类和其他类型之间的关系。

类成员

这里有一个最基本的类——一个空的类:

class Point {}

这个类还不是很有用,所以我们开始添加一些成员。

类属性

在一个类上声明字段,创建一个公共的可写属性:A mapped type is a generic type which uses aunion of PropertyKey s (frequently created via a keyof ) to iterate through keys to create a type:

class Point {
    x: number;
    y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;

与其他位置一样,类型注解是可选的,但如果不指定,将是一个隐含的 any 类型。

字段也可以有初始化器;这些初始化器将在类被实例化时自动运行。