⤴Top⤴

TypeScript 问题收集

博客分类: 前端
修改内容:add infer keyword

TypeScript 问题收集

TypeScript 问题收集

更新点

import type

在以往版本中,如果我们要导入一个值或类型,直接全部导入即可,在 TS 转 JS 的时候,TS 会识别出那些导入项被当做类型使用,并将其删除。但是有时候 TS 并不能识别我们导入的到底是值还是类型。Typescript 3.8 里新增了一个新的语法 import type,即仅导入类型。同样 export type 仅提供一个用于类型的导出,最终都会被删除:

import type { SomeThing } from "./some-module.js"

export type { SomeThing }

if you’ve hit issues under --isolatedModules, TypeScript’s transpileModule API, or Babel, this feature might be relevant.

需要注意的是,当使用仅类型导入功能时,导入 class 的话不能对其进行扩展:

import type { Component } from "react"

interface ButtonProps {
  // ...
}

class Button extends Component<ButtonProps> {
  //                 ~~~~~~~~~
  // error! 'Component' only refers to a type, but is being used as a value here.
  // ...
}

号外号外,4.5 版本又更新了写法:

- import type { BaseType } from "./some-module.js"
- import { someFunc } from "./some-module.js"
+ import { someFunc, type BaseType } from "./some-module.js"

Top-Level Await

一般情况下我们在使用 await 语法的时候,必须包裹在 async 函数中,然后立即调用该函数来执行异步操作:

async function main() {
  const response = await fetch("...");
  const greeting = await response.text();
  console.log(greeting);
}

main().catch((e) => console.error(e));

Typescript 3.8 新增了 Top-Level Await 来省去这部分包装代码。此外注意一点,Top-Level await 只在顶级模块工作,所以代码中需要含有 export 或者 import 才会认为该文件是一个模块。对于没有依赖的情况下,可以使用 export {}:

// 顶级模块
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

bigint

Typescript 3.2 针对于 BigInt 类型做了支持,表示为小写的 bigint。需要注意的是,bigint 和 number 是两种不同的类型:

declare let foo: number;
declare let bar: bigint;

foo = bar; // error: Type 'bigint' is not assignable to type 'number'.
bar = foo; // error: Type 'number' is not assignable to type 'bigint'.

// 混合计算使用的时候,必须将数字类型转换为 bigint
console.log(3.141592 * 10000n); // error
console.log(3145 * 10n); // error
console.log(BigInt(3145) * 10n); // okay!

@ts-nocheck / @ts-ignore

为了让用户能从 JS 到 TS 平滑过渡,原先能通过 --allowJs 来支持 TS 和 JS 文件的混合在一起编译,不会对 JS 文件去做类型检查。Typescript 2.3 可以通过两种方式来实现对混合在一起的 JS 文件一起做类型检查:

  1. 通过开启 --checkJs 配置来对 JS 文件去做检测。在 JS 文件中,可以通过在顶部添加 @ts-nocheck 注释跳过该文件类型检测,也可以通过添加 @ts-ignore 注释跳过下一行的文件类型检测
  2. 直接通过 @ts-check 对 JS 文件去做检测

并且做类型检测的时候,还可以根据 jsdoc 的类型注解自动来进行判断:

// @ts-check

/**
 * @param {string} input
*/
function foo ( input ) {
  input.tolowercase ()
      // ~~~~~~~~~~~ Error!  Should be toLowerCase
} 

到了 Typescript 3.7@ts-nocheck 的注释也可以不仅仅针对 JS 文件,同样可以作用于 TS 文件。

到了 Typescript 4.1,设置 --checkJs 的时候会默认开通 --allowJS

@ts-expect-error

当我们通过 @ts-expect-error 来进行注释时,TS 抛出的类型错误会被忽略;如果没有错误,则会抛出异常 Unused '@ts-expect-error' directive.。通常我们的使用场景可能是单元测试,比如:

function doStuff(abc: string, xyz: string) {
  assert(typeof abc === "string");
  assert(typeof xyz === "string");

  // do some stuff
}
// 单元测试
expect(() => {
  doStuff(123, 456);
}).toThrow();

如果单元测试也是用 TS 来编写的话,那么我们可以看到如下的报错:

doStuff(123, 456);
//           ~~~
// error: Type 'number' is not assignable to type 'string'.

因此通过上述的注释我们就能跳过这个类型检测。但有一个问题,它在功能上和 @ts-ignore 很相似呀,为啥要单独引入这个指令呢,你会 pick 谁?下面是官网贴出来的两者的比较:

  1. you’re writing test code where you actually want the type system to error on an operation
  2. you expect a fix to be coming in fairly quickly and you just need a quick workaround
  3. you’re in a reasonably-sized project with a proactive team that wants to remove suppression comments as soon affected code is valid again
  1. you have a larger project and new errors have appeared in code with no clear owner
  2. you are in the middle of an upgrade between two different versions of TypeScript, and a line of code errors in one version but not another.
  3. you honestly don’t have the time to decide which of these options is better.

Template Literal Types

Typescript 4.1 里引入了字符串模板类型,简单的看下:

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
//   ^ = type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"

我们也可以举个实际一点的栗子,比如要传入一些位移参数:

type VerticalAlignment = "top" | "middle" | "bottom"
type HorizontalAlignment = "left" | "center" | "right"

// Takes
//   | "top-left"    | "top-center"    | "top-right"
//   | "middle-left" | "middle-center" | "middle-right"
//   | "bottom-left" | "bottom-center" | "bottom-right"

declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void

setAlignment("top-left");   // works!
setAlignment("top-middel"); // error!
// Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.

再看个稍微复杂点的栗子:

type PropEventSource<T> = {
  on<K extends string & keyof T>
    (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

let person = makeWatchedObject({
  firstName: "Homer",
  age: 42,
  location: "Springfield",
});

// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
  // 'newName' has the type of 'firstName'
  console.log(`new name is ${newName.toUpperCase()}`);
});

// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
  if (newAge < 0) {
    console.log("warning! negative age");
  }
})

Key Remapping in Mapped Types

映射类型可以基于任意键创建新的对象类型,打个比方:

type Options = {
  [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
//   type Options = {
//     noImplicitAny?: boolean,
//     strictNullChecks?: boolean,
//     strictFunctionTypes?: boolean
//   };

但是我们目前能够做的只是在给出的值里面去生成 key,而不能映射到其他 key 值,Typescript 4.1 通过 as 提供了重新映射的功能:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

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

type LazyPerson = Getters<Person>
//   ^ = type LazyPerson = {
//       getName: () => string;
//       getAge: () => number;
//       getLocation: () => string;
//   }

我们还可以利用这个实现 Omit 的效果:

// Remove the 'kind' property
type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
  kind: "circle"
  radius: number
}

type KindlessCircle = RemoveKindField<Circle>;
//   ^ = type KindlessCircle = {
//       radius: number
//   }

问题收集

union type with filter / map

对于一个变量是联合数组类型(union type)时,无法调用其 filter 或者 map 方法:

const person: string[] | number[] = []

person.filter((p: string | number) => v !== null)

filter map

很显然 person.filter 肯定是可调用方法,但是 TS 对于这种情况的提示却是 The expression is not callable。解决办法是可以把数组类型由联合数组变为数组元素为联合类型,如下。推荐阅读:TypeScript 3.3

const person: <string| number>[] = []

Operands for delete must be optional

当使用 delete 操作符,运算元必须为 any, unknown, never,或者为可选(因为其类型中包含了 undefined)。否则会报错:

interface Thing {
  prop: string
}

function f(x: Thing) {
  delete x.prop // The operand of a 'delete' operator must be optional.
}

修改的方式也很简单,只要把相关属性改为 optional 即可:

interface Thing {
  prop?: string
}

如果碰到比较复杂的栗子,比如接口定义了多个属性的话,还可以用以下自定义类型去修改:

export type TPickPartial<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>

// 例如
type TTest = TPickPartial<TAnotherTest, 'canSort' | 'scenes'>

Object.keys using numbers

调用 Object.keys 获取对象的 key 值,在类型检测时默认为 string[],这样就会有个问题:

type Foo = { [key: number]: string }

const foo: Foo = { 100: 'foo', 200: 'bar' }
const sizes: number[] = Object.keys(foo) // Type 'string[]' is not assignable to type 'number[]

因此我们必须将字符串类型先转换为数字,最简单的方法是通过 map 来实现:

const sizes: number[] = Object.keys(foo).map(Number)

来看个完整一点的栗子:

const weekMapping = {
  1: '周一',
  2: '周二',
  3: '周三',
  4: '周四',
  5: '周五',
  6: '周六',
  7: '周日',
}

type TWeekKey = keyof typeof weekMapping

const keys = Object.keys(weekMapping).map(Number) as Array<TWeekKey>

function WeekSelector() {
  return (
    keys.map(key => (
      <Button key={key}>{weekMapping[key]}</Button>
    ))
  )
}

分布式条件类型 isDistributive

这节主要参考这里,先看看以下 res 的类型都是啥:

// 场景一
type TTest<T> = T extends number ? 1 : 2
type res = TTest<1 | 'a'>

// 场景二
type TTest<T> = T extends true ? 1 : 2
type res = TTest<boolean>

// 场景三
type TTest<T> = T extends true ? 1 : 2
type res = TTest<any>

// 场景四
type TTest<T> = T extends true ? 1 : 2
type res = TTest<never>

问题解答如下:

<!-- 场景一 -->
答案:1 | 2
原因:对于分布式条件类型,联合类型的每个类型会单独传入求值,把每个的结果合并成联合类型

<!-- 场景二 -->
答案:1 | 2
原因:boolean 其实也是联合类型,所以会把 true 和 false 分别传入求值,同上

<!-- 场景三 -->
答案:1 | 2
原因:条件类型对 any 做了特殊处理,如果左边是 any,那么直接把 trueType 和 falseType 合并成联合类型返回

<!-- 场景四 -->
答案:never
原因:当条件类型左边是 never 时,直接返回 never

我们根据源码来解释下,TypeScript 在处理到条件类型 Conditional Type 的时候,会设置一个 isDistributive 的属性,根据类型参数是不是 checkType(左边的类型)来设置:

function getTypeFromConditionalTypeNode(node: ConditionalTypeNode): Type {
  const links = getNodeLinks(node);
  if (!links.resolvedType) {
    const checkType = getTypeFromTypeNode(node.checkType);
    const aliasSymbol = getAliasSymbolForTypeNode(node);
    const aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol);
    const allOuterTypeParameters = getOuterTypeParameters(node, /*includeThisTypes*/ true);
    const outerTypeParameters = aliasTypeArguments ? allOuterTypeParameters : filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node));
    const root: ConditionalRoot = {
      node,
      checkType,
      extendsType: getTypeFromTypeNode(node.extendsType),
      isDistributive: !!(checkType.flags & TypeFlags.TypeParameter),
      inferTypeParameters: getInferTypeParameters(node),
      outerTypeParameters,
      instantiations: undefined,
      aliasSymbol,
      aliasTypeArguments
    };
    links.resolvedType = getConditionalType(root, /*mapper*/ undefined);
    if (outerTypeParameters) {
      root.instantiations = new Map<string, Type>();
      root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType);
    }
  }
  return links.resolvedType;
}

因为 T extends number 的 checkType 是 T,所以这里的 isDistributive 就是 true,也就是它是分布式条件类型。这时会在求值的时候把每个类型单独传入求值,最后把结果合并。这就是分布式条件类型遇到联合类型时的处理:

// Distributive conditional types are distributed over union types. For example, when the
// distributive conditional type T extends U ? X : Y is instantiated with A | B for T, the
// result is (A extends U ? X : Y) | (B extends U ? X : Y). 👈
result = distributionType && checkType !== distributionType && distributionType.flags & (TypeFlags.Union | TypeFlags.Never) ?
  mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper)), aliasSymbol, aliasTypeArguments) :
  getConditionalType(root, newMapper, aliasSymbol, aliasTypeArguments);

场景一和二都会走 mapTypeWithAlias 这个方法。场景三的 any 会发现走了 getConditionalType 这个方法,也确定了 any 不属于 union 联合类型,继续往下看,意思就是返回 trueType 和 falseType 的联合类型,因为 any 匹配任何类型,也就得出最终答案 1 | 2:

// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any && !isUnwrapped) {
  (extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
}

至于 never 我们可以看到往下走了这段代码,直接返回 never 了:

if (type.flags & TypeFlags.Never) {
  return type;
}

总结一下:条件类型当 checkType(左边的类型)是类型参数的时候,会有 distributive 的性质,也就是传入联合类型时会把每个类型单独传入做计算,最后把结果合并返回。这叫做分布式条件类型。

具体源码的解释请参考这里 👈

infer 类型推断

infer 表示在 extends 条件语句中待推断的类型变量:

type ParamType<T> = T extends (...args: infer P) => any ? P : T;
interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void;

type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string

在 2.8 版本中,TypeScript 内置了一些与 infer 有关的映射类型:

type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any

相比于文章开始给出的示例,ReturnType<T> 只是将 infer P 从参数位置移动到返回值位置,因此此时 P 即是表示待推断的返回值类型:

type Func = () => User;
type Test = ReturnType<Func>; // Test = User

同样的,一个构造函数可以使用 new 来实例化,因此它的类型通常表示如下:

type Constructor = new (...args: any[]) => any

当 infer 用于构造函数类型中,可用于参数位置 new (...args: infer P) => any 和返回值位置 new (...args: any[]) => infer P

// 获取参数类型
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any
  ? P
  : never;

// 获取实例类型
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

class TestClass {
  constructor(public name: string, public age: number) {}
}

type Params = ConstructorParameters<typeof TestClass>; // [string, number]

type Instance = InstanceType<typeof TestClass>; // TestClass

此外可参考 weeklycoditional infer 👈