TypeScript

无法在不重载函数签名的情况下来实现pipe/flow/compose(这三个函数的本质相同).

当前编写高阶类型时, 语法过于牵强, 比如用extends实现条件语句.
理应有像typetype那样的, 更符合常识的编写方法.

TypeScript当前只能允许特定类型, 而不能禁止特定类型.

  • https://github.com/microsoft/TypeScript/issues/33353
  • https://github.com/microsoft/TypeScript/issues/43632
  • https://github.com/microsoft/TypeScript/issues/27594
  • https://github.com/microsoft/TypeScript/issues/38519
enum Enum {
A = 0
}
const val = 0
const result = val in Enum // true

字符串枚举没有反向映射, 所以不能使用 in 运算符.

enum Enum {
A = 'a'
}
const val = 'a'
const result = Object.values(Enum).includes(val) // true
enum Enum {
A
}
const a = Enum.A
const nameOfA = Enum[a] // 'A'

常量枚举在编译时会使用内联形式直接替换对应值, 因此与枚举相关的信息在编译后的代码里完全删除.

const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
]
// 编译后
let directions = [
0 /* Up */,
1 /* Down */,
2 /* Left */,
3 /* Right */,
]

当一个类被创建的目的是为了实现一个只包含可选属性的接口时, 使用该类的实例可能会遇到错误:
TS2559: Type 'X' has no properties in common with type 'Y'.

这是因为类没有将接口声明的全部属性包括在类的定义里.
有两种方法可以解决此错误:

  • 将类的定义改成与接口相同(不推荐, 因为可能需要改动很多).
  • 将实例的类型强制转换为接口的类型, 可以用工厂函数方便实现此功能.

参考: https://stackoverflow.com/questions/46449237/type-x-has-no-properties-in-common-with-type-y

  • ts-toolbelt: https://github.com/millsp/ts-toolbelt
  • purify: https://github.com/gigobyte/purify
  • <<typetype>>: https://github.com/mistlog/typetype
  • type-challenges: https://github.com/type-challenges/type-challenges
  • fp-ts: https://github.com/gcanti/fp-ts

type不是一个单独的类型, 而是其代表的类型的别名.
两个代表的类型相同的type是完全相等的, 无法以任何方式区别它们.

从TypeScript 4.1开始, 支持直接修改映射类型的键名.

type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
}
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, 'kind'>]: Type[Property]
}

Object{} 是相同的, 接受的值相当于 any.
object 是在TypeScript 2.2中引入的, 指代指除primitive values以外的任何值.
然而, object 类型很难直接使用, 因为它没有property定义.

在不开启strictNullChecks的情况下, 这三种类型都接受 undefinednull 值.
为避免这三种类型接受 undefinednull, 一定要开启strictNullChecks选项.

当用于函数的返回类型时,
void 表示忽略函数的返回类型, undefined 则指定函数返回 undefined 类型.
以 =void= 为返回类型的函数签名可以兼容具有任何返回值的函数.

type A = () => void
type B = () => undefined
const a: A = () => true // ok
const b: B = () => true // oops!

unknown 被认为是类型安全版本的 any.
unknown 在静态检查时会报错, 而 any 会忽略一切错误.

定义函数签名时, 可以额外设置prop, 只需要让它看起来像个接口.

// 类型形式
type DescribableFunction = {
(someArg: number): boolean
description: string
}
// 接口形式
interface DescribableFunction {
(someArg: number): boolean
description: string
}
// 类型形式
type SomeConstructor = {
new (someArg: string): SomeObject
}
// 接口形式
interface SomeConstructor {
new (someArg: string): SomeObject
}
// 同时作为一般函数和构造函数
interface CallOrConstruct {
new (s: string): Date
(n?: number): number
}

将函数签名作为约束的关键在于如果args是任意的,
则类型需要是 anyany[], 不能是 unknownunknown[].

T extends (...args: any) => any

很容易将 & 误解为混入(mixin), 这是因为当两个接口的成员不重叠时, 其行为是符合开发人员预期的.
但如果两个接口的成员有重叠, 则会返回一个无法使用的接口(即带有never的接口), 这与混入有很大的差异.

// 不同的类型
type Foo = string
type Bar = number
type Result = Foo & Bar // never
// 有重叠的union类型
type Foo = string | boolean
type Bar = number | boolean
type Result = Foo & Bar // boolean
// 有重叠的接口
interface Foo {
prop: string
a: string
}
interface Bar {
prop: number
b: string
}
type Result = Foo & Bar // { a: string; b: string; prop: never }
// 当接口里存在值类型为never的必要字段时, 该接口无法使用, 因此不应该在有重叠的接口上使用&.
// 可选与必需成员重叠的接口
interface Foo {
prop?: string
}
interface Bar {
prop: string
}
type Result = Foo & Bar // { prop: string }
type Result = Bar & Foo // 该运算符合交换律, 即使调换顺序也会得到 { prop: string }
// 不重叠的接口
interface Foo {
a: string
}
interface Bar {
b: number
}
type Result = Foo & Bar // { a: string; b: number }
// 不同的类型
type Foo = string
type Bar = number
type Result = Foo | Bar // string | number
// 有重叠的union类型
type Foo = string | boolean
type Bar = number | boolean
type Result = Foo | Bar // string | number | boolean
// 有重叠的接口
interface Foo {
prop: string
a: string
}
interface Bar {
prop: number
b: number
}
type Result = Foo | Bar
// | { prop: string; a: string }
// | { prop: number; b: number }
// | { prop: string | number; a: string; b: number }
// 可选与必需成员重叠的接口
interface Foo {
prop?: string
}
interface Bar {
prop: string
}
type Result = Foo | Bar // { prop?: string | undefined }
type Result = Bar | Foo // 该运算符合交换律, 即使调换顺序也会得到 { prop?: string | undefined }
// 不重叠的接口
interface Foo {
a: string
}
interface Bar {
b: number
}
type Result = Foo | Bar
// | { a: string }
// | { b: number }
// | { a: string; b: number }

自TypeScript 4.3开始, 类的成员新增了override修饰符, 用来表明该成员覆盖了父类的同名成员.
当添加override修饰符时, TypeScript会检查父类是否存在同名成员.
TypeScript提供了一个flag, 可以强制要求那些覆盖父类成员的子类成员使用override修饰符.

Record的定义:

type Record<K extends keyof any, T> = {
[P in K]: T
}

当K的值是类型时, 它与 index signature 没有区别.

当K的值是字符串union时, 它与 index signature 不同, 如下:

type A = Record<'a' | 'b', string>
// 等价于
interface A {
a: string
b: string
}

https://github.com/microsoft/TypeScript/issues/27024

export type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? true
: false
type Box<T> = { value: T }
type ResultA = Equals<Box<string>, Box<any>> // false
type ResultB = Equals<Box<string>, Box<unknown>> // false
type ResultC = Equals<Box<any>, Box<string>> // false
type ResultD = Equals<Box<unknown>, Box<string>> // false
type Box<T> = { value: T }
type ResultA = Box<string> extends Box<any> ? true : false // true
type ResultB = Box<string> extends Box<unknown> ? true : false // true
type ResultC = Box<any> extends Box<string> ? true : false // true
type ResultD = Box<unknown> extends Box<string> ? true : false // false
type ResultE = {} extends Box<string> ? true : false // false
type ResultF = { value: any, other: unknown } extends Box<string> ? true : false // true
type ResultG = { value: unknown, other: unknown } extends Box<string> ? true : false // false
// 需要注意的是, 尽管extends最常见的形式是下面这种(形式上符合OOP的extends):
T extends ...
// 但extends的左值还可以是一个计算值, 所以下面的语法也是正确的:
((...args: T) => any) extends ...

TypeScript可以直接读取对应索引的值, 甚至可以读取元组的length属性.

type Tuple = [string, number]
type FirstOfTuple = Tuple[0] // string
type Length = Tuple['length'] // 2

递归类型需要组合使用几种高级技巧,
其中最关键的部分是创建一个动态的对象类型, 将这个对象类型的索引当作分支条件使用.

type Head<T extends unknown[]> =
T extends [infer U, ...unknown[]]
? U
: never
type Tail<T extends unknown[]> =
T extends [unknown, ...infer U]
? U
: []
type HasTail<T extends unknown[]> =
T extends [] | [unknown]
? false
: true
// 递归类型
type Last<T extends unknown[]> = {
0: Head<T>
1: Last<Tail<T>>
}[HasTail<T> extends true ? 1 : 0]

官方文档提供了常见模板: https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html

TypeScript里的 .ts 文件分为两种:

  • 模块: 包含顶级 import / export.ts 文件
  • 全局脚本: 不包含顶级 import / export.ts 文件

全局脚本将自动被项目的其他脚本引用, 无需手动导入即可使用其中的类型.
模块中的类型需要手动导入.

因此, .d.ts 文件仅用于为开发人员注明"这是一个不包含 import / export 的全局脚本",
文件后缀本身并不影响功能.
创建一个符合"全局脚本"定义的 .ts 文件同样也能为项目添加全局类型和接口.

这可以通过import函数实现, 不会破坏全局脚本的属性.

interface Test {
value: import('other-module').Type
}

declare 向环境添加一个已存在的标识符的类型声明.
TypeScript项目自动生成的类型文件就是通过 declare 声明类型的, 作用类似于C语言的头文件.

// 声明一个当前环境下已存在的函数签名
declare function foo(): void
// 声明一个当前环境下已存在的常量
declare const BAR: string

以非侵入的形式为已经存在的模块创建类型声明.

declare module "my-module" {
...
}

模块名还支持通配符, 以适用于Webpack等打包器.

declare namespace 适用于声明一个已存在的对象的类型.
尽管声明对象的类型也可以用 declare const NAME: Interface 完成,
但namespace胜在其内部是一个与外部无关的作用域,
因此可以在里面实施内部类型等有助于提升代码可读性的做法.

declare namespace 最常见的使用场景是用来声明以全局变量形式导入的JavaScript库.

可以通过TypeScript的声明合并特性实现, Window 接口的成员会被并入已存在的 Window 接口.

使用这种做法时, 只有通过 window.foo 才能找到新添加的成员.
无法通过 global.foo, globalThis.foo, 或直接以标识符访问全局变量的方式 foo 找到它.

如果文件是一个模块, 声明合并无法扩散到模块之外, 需要使用 declare global.

declare global {
interface Window {
foo: string
}
}

如果文件是一个全局脚本, 就没有必要使用 declare global, 直接就能声明合并.

interface Window {
foo: string
}