本文翻译自 stefan baumgartner 的 《TypeScript: Built-in generic types》,原文:TypeScript: Built-in generic types

TypeScript 附带大量内置泛型,可以简化你的开发工作流程。下面是所有内置泛型的列表,并附有示例。

注意:此列表可能(几乎肯定)不完整。如果你遗漏了什么,并且想要添加它,请通过 Twitter 联系我。

本文提到了:

  • Readonly
  • ReadonlyArray
  • ReturnType
  • Partial
  • Required
  • NonNullable
  • Pick
  • Record
  • Extract
  • Exclude
  • Omit

Readonly

JavaScript 中的 const 很狡猾,因为它虽然意味着你不能将任何其他值重新分配给此常量,但是允许更改对象的属性。Readonly 内置类型可以解决这个问题。

type Point = {
  x: number,
  y: number
};

const p: Readonly<Point> = {
  x: 4,
  y: 2
};

p.x = 5; // ❌ 编译错误!

ReadonlyArray

ReadonlyArray 允许我们在使用改变原始数组的数组函数时抛出错误。

const values: ReadonlyArray<number> = [1, 2, 3, 4, 5];

values.push(6); // ❌ 编译错误!这会使数组发生变化
values.filter(x => x > 4); // ✅ 编译通过!filter 返回新数组

如果你想要让一个数组不可变(immutable),这样做会很方便。

ReturnType

此内置类型为你提供任何函数的返回类型。

type Point = {
  x: number,
  y: number
}

function generateSquare(bottomLeft: Point, topRight: Point) {
  return {
    bottomLeft,
    bottomRight: {
      x: topRight.x,
      y: bottomLeft.y,
    },
    topLeft: {
      x: bottomLeft.x,
      y: topRight.y
    },
    topRight
  }
}

type Square = ReturnType<typeof generateSquare>; 
// 现在我可以在代码的任何地方使用此返回类型。

function areaOfSquare(square: Square): number {
  //做一些事情...
  return result;
}

您还可以访问类内的函数:

class Square {
  generate(bottomLeft, topRight) {
    return {
      bottomLeft,
      bottomRight: {
        x: topRight.x,
        y: bottomLeft.y,
      },
      topLeft: {
        x: bottomLeft.x,
        y: topRight.y
      },
      topRight
    }
  }
}

type TSquare = ReturnType<Square['generate']>;
declare let result: TSquare;

Partial

Partial<T> 从一个类型中获取所有属性,并使它们成为可选的。它有什么好处?试想现在有一组默认选项,当你想覆盖其中的部分属性时就很有用了。Partial<T> 可帮你针对该情况自动完成和类型检查:

const defaultOptions = {
  directory: '.',
  incremental: true, 
  filePattern: '**/*',
}

function start(options: Partial<typeof defaultOptions>) {
  const allOptions = Object.assign({}, defaultOptions, options);
  console.log(allOptions); 
}

start({
  incremental: false, // ✅ 类型检查通过
});

start({
  flatten: true // ❌ 报错,此属性与我们的选项无关
});

Required

Required<T>Partial<T> 相反,当 Partial<T> 让每个属性变得可选, Required<T> 让每个属性变成必须。

type Container = {
  width: number,
  height: number, 
  children?: Container[]
}

function getChildrenArea(parent: Required<Container>) {
  let sum = 0;
  for (let child of parent.children) {
    sum = sum + (child.width * child.height)
  }
  return sum;
}

const mainContainer: Container = {
  width: 200,
  height: 100
}

getChildrenArea(mainContainer); // ❌ 报错,缺少 children 属性

NonNullable

NonNullable<T> 帮你确保不会将 nullundefined 传递给函数。它会补充 strictNullChecks 编译器 flag,所以确保你是否要启用它。

function print<T>(x: NonNullable<T>) {
  console.log(x.toString());
}

print('Hello');
print(2);
print(null); // ❌ 报错
print(undefined); // ❌ 报错

Pick

使用 Pick<T, K extends keyof T>,你只需使用选定的属性列表,就可以从现有对象创建新类型。Lodash 的同名 pick 函数就是其用法的一个很好的例子:

/**
 * The pick function is generic as well. It has two generic types:
 * - T ... the type of the object we want to pick props from
 * - K ... a subset of all keys in T
 *
 * Our method signature takes an object of type T, the other parameters
 * are collected in an array of type K.
 *
 * The return type is a subset of keys of T.
 */
declare function pick<T, K extends keyof T>(obj: T, ...propsToPick: K[]): Pick<T, K>;

const point3D = {
  x: 2,
  y: 0,
  z: 4
}

const point2D = pick(point3D, 'x', 'y'); // 返回类型 { x: number, y: number }

当与其他泛型类型(例如 Exclude)一起使用时,此类型特别有用。

Record

Record<K, T> 很有趣。有了它,你可以说每个键 K 都应该是 T 类型。有了它,你可以做以下事情:

type Person = Record<'firstName' | 'lastName', string>

{firstName:string,lastName:string} 相同。或者类似于:

type MetaInfo = {
  title: string,
  url: string
}

type Episodes = Record<string, MetaInfo>

它允许对象具有任何可能的 key,但 value 类型为 MetaInfo。这与 {[k:string]:MetaInfo} 非常相似。

到现在为止还好。但是,如果我们可以使用其他方法获得类似的结果,为什么还要使用 Record 类型呢?Record 在处理其他泛型类型时很有帮助。让我们看一下该示例:我们可以创建一个函数,将对象所有 key 转换为字符串表示形式:

// 它将所有值转换为字符串。
declare function allToString<T>(obj: T): Record<keyof T, string>;

const person = {
  firstName: 'Stefan',
  lastName: 'Baumgartner',
  age: Number.MAX_VALUE
}

// 现在 strPerson 中的所有属性都是字符串
const strPerson = allToString(person);

Extract

Extract<T, K> 从 T 中提取可赋值给 K 的所有类型。假设您有两种不同类型的形状。圆形和矩形。它们看起来就像这样:

const rect = {
  width: 200,
  height: 100,
  area() {
    return this.width * this.height;
  },
  perimeter() {
    return this.width * 2 + this.height * 2;
  }
}

const circle = {
  r: 50,
  area() {
    return this.r * this.r * Math.PI;
  },
  perimeter() {
    return 2 * this.r * Math.PI;
  }
}

它们的类型是:

type Rect = typeof rect;
type Circle = typeof circle;

它们有一个共同点:它们都有相同的面积和周长计算方法。这些对象可能会随着时间的推移而更改。我们仍然希望确保我们只访问在这两个方法中都可用的方法。使用 Extract,我们可以获得这些函数的 key:

// 提取: 'area' | 'perimeter'
type ShapeFunctionProps = Extract<keyof Rect, keyof Circle>

要再次创建可以访问所有这些函数的类型,我们可以使用前面的 Record 类型:

type ShapeFunctions = Record<ShapeFunctionProps, () => number>

现在,我们在函数中有了可能适用于所有这些对象的额外类型安全:

declare function extensiveMath<T extends ShapeFunctions>(obj: T)

extensiveMath(rect); // ✅ 通过编译
extensiveMath(circle); // ✅ 通过编译

Exclude

Exclude<T, K> 从 T 中排除可赋值给 K 的所有类型。它类似于 Extract 的反义词。让我们以前面的例子为例:

type Rect = typeof rect;
type Circle = typeof circle;

// 只有 'width' | 'height';
type RectOnlyProperties = Exclude<keyof Rect, keyof Circle>;

// An object of type { width: number, height: number }
type RectOnly = Pick<Rect, RectOnlyProperties>;

declare function area<T extends RectOnly>(obj: T)

area({
  width: 100,
  height: 100
})  // ✅ 编译通过;

Omit

从 3.5 版本开始,TypeScript 包括 Omit<T,K> 类型。Omit<T,K>Pick<T,Exclude<key of T,K>> 的快捷方式,但由于使用频率很高,团队决定添加它:

type Person = {
    name: string;
    age: number;
    location: string;
};

type Cosmopolitan = Omit<Person, 'location'>;

// 相等于
type Cosmopolitan = {
    name: string;
    age: number;
};

最后

这些是可能对不同情况有用的内置泛型。我认为如果你想要在不过受多干扰的情况下强化你的库或应用程序,所有这些泛型类型的组合是非常有帮助的。特别是当你为接受不同参数类型的方法生成类型定义时(就像 JavaScript 中经常发生的那样),它们可能比自己定义每个可能的类型结果更有用。

再说一次,我肯定我还有很多不知道的。所以如果您有任何想要在这里进行转换的内置泛型,请让我知道

更新:我的伙伴 Martin Hochel 有几个建立在这些类型之上的映射类型。它们相当不错,所以快来看看吧。