文章目录

  • 一、 基本关键词
  • 1.1 keyof 索引查询
  • 1.2 Typeof 类型运算符
  • 1.3 索引访问
  • 1.4 模板字符串类型
  • 1.5 条件类型 & extends关键词特性 & infer条件推断
  • 1.6 映射类型
  • 1.7 类型兼容
  • 二、Ts内置类型
  • 2.1 Partial实现
  • 2.2 Readonly 实现
  • 2.3 Pick 实现
  • 2.4 Record 实现
  • 2.5 Exclude 实现
  • 2.6 Extract
  • 2.7 Omit原理解析
  • 2.8 Parameters
  • 2.9 ReturnType
  • 三、类型保护
  • 3.1 in 关键字
  • 3.2 typeof 关键字
  • 3.3 instanceof 关键字
  • 3.4 自定义类型保护的类型谓词
  • 四、关于使用ts的一些建议
  • 五、探索


一、 基本关键词

1.1 keyof 索引查询

keyof 用来对对象类型索引遍历,并生成其键的字符串或数字联合类型, 对应任何类型T,keyof T的结果为该类型上所有公有属性key的联合

type Point = { x: number; y: number };
type P = keyof Point;
// p = 'x' | 'y'

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
 
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // ?


type test = keyof any; // ?


interface Eg1 {
  name: string,
  readonly age: number,
}

type T1 = keyof Eg1 // ?

class Eg2 {
  private name: string;
  public readonly age: number;
  protected home: string;
}

type T2 = keyof Eg2 // ?
  1. 在 TypeScript 中支持两种索引签名,数字索引和字符串索引:为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引
  2. T2实则被约束为 age,而name和home不是公有属性,所以不能被keyof获取到。

1.2 Typeof 类型运算符

typeof运算符可以在_类型_上下文中使用它来引用变量或属性的_类型:_

let s = "hello";
let n: typeof s;

这对于基本类型不是很有用,但结合其他类型运算符,您可以typeof方便地表达许多模式。例如,让我们从查看预定义类型开始ReturnType。它接受一个_函数类型_并产生它的返回类型:

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

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

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

请记住,值_和_类型_不是一回事。要引用该_值的类型,我们使用:typeof,
注意:TypeScript 有意限制您可以使用的表达式类型typeof,具体来说,仅typeof在标识符(即变量名)或其属性上使用是合法的。这有助于避免编写您认为正在执行但不是的代码的混乱陷阱:

// error
let shouldContinue: typeof f();

1.3 索引访问

interface Eg1 {
  name: string,
  readonly age: number,
}

type V1 = Eg1['name']

type V2 = Eg1['name' | 'age']

type V2 = Eg1['name' | 'age2222']

type V3 = Eg1[keyof Eg1]

T[keyof T]的方式,可以获取到T所有key的类型组成的联合类型; T[keyof K]的方式,获取到的是T中的key且同时存在于K时的类型组成的联合类型; 注意:如果[]中的key有不存在T中的,则是any;因为ts也不知道该key最终是什么类型,所以是any;且也会报错;

索引类型本身就是一种类型,因此我们可以完全使用联合keyof、 或其他类型:

type Person = { age: number; name: string; alive: boolean };

type I1 = Person["age" | "name"];
 
type I2 = Person[keyof Person];
     
type AliveOrName = "alive" | "name";

type I3 = Person[AliveOrName];

使用任意类型进行索引的另一个示例是number用于获取数组元素的类型。我们可以结合它typeof来方便地捕获数组字面量的元素类型:

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

type Age = typeof MyArray[number]["age"];
     
type Age2 = Person["age"];

注意: 只能在索引时使用类型

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

// error
const key = "age";
type Age = Person[key];

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

1.4 模板字符串类型

模板字符串类型建立在js的模板字符串之上,并且能够通过联合扩展成许多字符串。

type World = "world";
 
type Greeting = `hello ${World}`;

当在插值位置使用联合时,类型是可以由每个联合成员表示的每个可能的字符串集合:

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
 
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

对于模板文字中的每个插值位置,联合是交叉相乘的:

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
  • 使用模板文字进行推理(我没看懂)
type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};

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

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

person.on("firstNameChanged", newName => {
    //                        ^?
    console.log(`new name is ${newName.toUpperCase()}`);
});

person.on("ageChanged", newAge => {
    //                  ^?
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})
  • 内在字符串操作类型
//将字符串中的每个字符转换为大写版本。
// Uppercase<StringType>
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
  
// 将字符串中的每个字符转换为等效的小写字母。
// Lowercase<StringType>
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">

// 将字符串中的第一个字符转换为等效的大写字母
//Capitalize<StringType>
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
  
// 将字符串中的第一个字符转换为等效的小写字母
//Uncapitalize<StringType>
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;

补充案例:去掉字符串前面的空格字符 (没看懂)

type Whitespace = ' ' | '\n' | '\r' | '\t'
type TrimStart<S extends string, P extends string = Whitespace> =
  S extends `${P}${infer R}` ? TrimStart<R, P> : S

type String1 = '\t  \r  \n   value'
type Trimmed1 = TrimStart<String1>

type String2 = '---value'
type Trimmed2 = TrimStart<String2, '-'>

1.5 条件类型 & extends关键词特性 & infer条件推断

extends 一般用于接口,表示继承,也可表示条件类型,可用于条件判断

  • 用于继承
// 用于继承
interface T1 {
  name: string,
}

interface T2 {
  sex: number,
}

interface T3 extends T1, T2 {
  age: number,
}

// 注意,接口支持多重继承,语法为逗号隔开。
// 如果是type实现继承,则可以使用交叉类型type A = B & C & D。
  • 条件判断,如果前面的条件满足,则返回问号后的第一个参数,否则第二个。类似于js的三元运算。
type A1 = 'x' extends 'x' ? 1 : 2;


type A2 = 'x' | 'y' extends 'x' ? 1 : 2;

type P<T> = T extends 'x' ? 1 : 2;

type A3 = P<'x' | 'y'>

提问:为什么A2和A3的值不一样?

  • 如果用于简单的条件判断,则是直接判断前面的类型是否可分配给后面的类型
  • 若extends前面的类型是泛型,且泛型传入的是联合类型时,则会依次判断该联合类型的所有子类型是否可分配给extends后面的类型(是一个分发的过程)。

总结,extends前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断。然后将最终的结果组成新的联合类型。
如果不想被分解(分发),做法也很简单,可以通过简单的元组类型包裹以下:

type P<T> = [T] extends ['x'] ? 1 : 2;

type A4 = P<'x' | 'y'>
  • 条件类型推断 infer

条件类型为我们提供了一种使用infer关键字方法来推断在真实分支中比较的类型,

  • infer关键词只能在extends条件类型上使用,不能在其他地方使用。
//实现一个推导数组所有元素的类型:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return 
     ? Return : never;

type Str = GetReturnType<(x: string) => string>;
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
type Num = GetReturnType<() => number>;

当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,会根据_最后一个_签名进行推断。无法根据参数类型列表执行重载决议。

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

当条件类型作用于泛型类型时,它们在给定联合类型时变得可_分配_

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

通常,分配性是期望的行为。extends为避免这种行为,您可以用方括号将关键字的每一侧括起来。

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

1.6 映射类型

映射类型建立在索引签名的语法之上,用于声明未提前声明的属性类型
一般我们声明接口:

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

映射类型是一种通用类型,它使用PropertyKeys 的联合(通常通过 akeyof创建)来遍历键以创建类型:

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

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

在映射期间可以应用两个额外的修饰符:它们分别影响可变性和可选性。可以通过- 和 + 来改变可变性和可选性

type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly 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 = Concrete<MaybeUser>;

在 TypeScript 4.1 及更高版本中,您可以使用映射类型中的as子句重新映射映射类型中的键:

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

可以通过条件类型生成 never 来过滤掉键:

// 移除kind属性
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
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 = EventConfig<SquareEvent | CircleEvent>

1.7 类型兼容

在集合论中,如果一个集合的所有元素在集合B中都存在,则A是B的子集;
类型系统中,如果一个类型的属性更具体,则该类型是子类型。(因为属性更少则说明该类型约束的更宽泛,是父类型)
因此,我们可以得出基本的结论**:子类型比父类型更加具体,父类型比子类型更宽泛。** 下面我们也将基于类型的可复制性(可分配性)、协变、逆变、双向协变等进行进一步的讲解。

  • 可赋值性
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

let a: Animal;
let b: Dog;

// 可以赋值,子类型更佳具体,可以赋值给更佳宽泛的父类型
a = b;
// 反过来不行
b = a;
  • 可赋值性在联合类型中的特性
type A = 1 | 2 | 3;
type B = 2 | 3;
let a: A;
let b: B;

// 不可赋值
b = a;
// 可以赋值
a = b;

是不是A的类型更多,A就是子类型呢?恰恰相反,A此处类型更多但是其表达的类型更宽泛,所以A是父类型,B是子类型。因此b = a不成立(父类型不能赋值给子类型),而a = b成立(子类型可以赋值给父类型)。

  • 协变
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

let Eg1: Animal;
let Eg2: Dog;
// 兼容,可以赋值
Eg1 = Eg2;

let Eg3: Array<Animal>
let Eg4: Array<Dog>
// 兼容,可以赋值
Eg3 = Eg4

通过Eg3和Eg4来看,在Animal和Dog在变成数组后,Array依旧可以赋值给Array,因此对于type MakeArray = Array来说就是协变的。

最后引用维基百科中的定义:
协变与逆变(Covariance and contravariance )是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
简单说就是,具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变的,而关系逆转了(子变父,父变子)就是逆变的。可能听起来有些抽象,下面我们将用更具体的例子进行演示说明:

  • 逆变
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

type AnimalFn = (arg: Animal) => void
type DogFn = (arg: Dog) => void

let Eg1: AnimalFn;
let Eg2: DogFn;
// 不再可以赋值了,
// AnimalFn = DogFn不可以赋值了, Animal = Dog是可以的
Eg1 = Eg2;
// 反过来可以
Eg2 = Eg1;

理论上,Animal = Dog是类型安全的,那么AnimalFn = DogFn也应该类型安全才对,为什么Ts认为不安全呢?看下面的例子:

let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => {
  arg.break();
}

// 假设类型安全可以赋值
animal = dog;
// 那么animal在调用时约束的参数,缺少dog所需的参数,此时会导致错误
animal({name: 'cat'});

从这个例子看到,如果dog函数赋值给animal函数,那么animal函数在调用时,约束的是参数必须要为Animal类型(而不是Dog),但是animal实际为dog的调用,此时就会出现错误。
因此,Animal和Dog在进行type Fn = (arg: T) => void构造器构造后,父子关系逆转了,此时成为“逆变”。

  • 双向协变

Ts在函数参数的比较中实际上默认采取的策略是双向协变:只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息(典型的就是上述的逆变)。 但是实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式:

// lib.dom.d.ts中EventListener的接口定义
interface EventListener {
  (evt: Event): void;
}
// 简化后的Event
interface Event {
  readonly target: EventTarget | null;
  preventDefault(): void;
}
// 简化合并后的MouseEvent
interface MouseEvent extends Event {
  readonly x: number;
  readonly y: number;
}

// 简化后的Window接口
interface Window {
  // 简化后的addEventListener
  addEventListener(type: string, listener: EventListener)
}

// 日常使用
window.addEventListener('click', (e: Event) => {});
window.addEventListener('mouseover', (e: MouseEvent) => {});
  • infer推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型
type Bar<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
} ? U : never;

// type T1 = string
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;

// type T2 = never
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
  • infer推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型
type Foo<T> = T extends {
  a: infer U;
  b: infer U;
} ? U : never;

// type T1 = string
type T1 = Foo<{ a: string; b: string }>;

// type T2 = string | number
type T2 = Foo<{ a: string; b: number }>;

二、Ts内置类型

2.1 Partial实现

/**
 * 核心实现就是通过映射类型遍历T上所有的属性,
 * 然后将每个属性设置为可选属性
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
}

// 扩展一下
type PartialOptional<T, K extends keyof T> = {
  [P in K]?: T[P];
}


type Eg1 = PartialOptional<{
  key1: string,
  key2: number,
  key3: ''
}, 'key1' | 'key2'>;

解析:

  • [P in keyof T]通过映射类型,遍历T上的所有属性
  • ?:设置为属性为可选的
  • T[P]设置类型为原来的类型

2.2 Readonly 实现

/**
 * 主要实现是通过映射遍历所有key,
 * 然后给每个key增加一个readonly修饰符
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}


type Eg = Readonly<{
  key1: string,
  key2: number,
}>

2.3 Pick 实现

挑选一组属性并组成一个新的类型。

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

2.4 Record 实现

/**
 * 核心实现就是遍历K,将值设置为T
 */
type Record<K extends keyof any, T> = {
  [P in K]: T
}

/**
 * @example
 * type Eg2 = {a: B, b: B}
 */
interface A {
  a: string,
  b: number,
}
interface B {
  key1: number,
  key2: string,
}
type Eg2 = Record<keyof A, B>
  • Partial、Readonly和Pick都属于同态的,即其实现需要输入类型T来拷贝属性,因此属性修饰符(例如readonly、?:)都会被拷贝。
  • Record是非同态的,不需要拷贝属性,因此不会拷贝属性修饰符

可以看到Pick的实现中,注意P in K(本质是P in keyof T),T为输入的类型,而keyof T则遍历了输入类型;而Record的实现中,并没有遍历所有输入的类型,K只是约束为keyof any的子类型即可。
最后再类比一下Pick、Partial、readonly这几个类型工具,无一例外,都是使用到了keyof T来辅助拷贝传入类型的属性。

2.5 Exclude 实现

Exclude<T, U>提取存在于T,但不存在于U的类型组成的联合类型。

/**
 * 遍历T中的所有子类型,如果该子类型约束于U(存在于U、兼容于U),
 * 则返回never类型,否则返回该子类型
 */
type Exclude<T, U> = T extends U ? never : T;

/**
 * @example
 * type Eg = 'key1'
 */
type Eg = Exclude<'key1' | 'key2', 'key2'>
  • never表示一个不存在的类型
  • never与其他类型的联合后,是没有never的
  • 因此上述Eg其实就等于key1 | never,也就是type Eg = key1
type Eg2 = string | number | never

2.6 Extract

Extract<T, U>提取联合类型T和联合类型U的所有交集。

type Extract<T, U> = T extends U ? T : never;


type Eg = Extract<'key1' | 'key2', 'key1'>

2.7 Omit原理解析

Omit<T, K>从类型T中剔除K中的所有属性。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;


// 另外一种实现
type Omit2<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}

2.8 Parameters

Parameters 获取函数的参数类型,将每个参数类型放在一个元组中。

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

type Eg = Parameters<(arg1: string, arg2: number) => void>;
  • Parameters首先约束参数T必须是个函数类型,所以(…args: any) => any>替换成Function也是可以的
  • 具体实现就是,判断T是否是函数类型,如果是则使用inter P让ts自己推导出函数的参数类型,并将推导的结果存到类型P上,否则就返回never;
  • type Eg = [arg1: string, arg2: number]这是一个元组,但是和我们常见的元组type tuple = [string, number]。官网未提到该部分文档说明,其实可以把这个作为类似命名元组,或者具名元组的意思去理解。实质上没有什么特殊的作用,比如无法通过这个具名去取值不行的。但是从语义化的角度,个人觉得多了语义化的表达罢了。
  • 定义元祖的可选项,只能是最后的选项
/**
 * 普通方式
 */
type Tuple1 = [string, number?];
const a: Tuple1 = ['aa', 11];
const a2: Tuple1 = ['aa'];

/**
 * 具名方式
 */
type Tuple2 = [name: string, age?: number];
const b: Tuple2 = ['aa', 11];
const b2: Tuple2 = ['aa'];

2.9 ReturnType

获取函数的返回值类型。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

三、类型保护

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

3.1 in 关键字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + );
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

3.2 typeof 关键字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型保护只支持两种形式:typeof v === “typename” 和 typeof v !== typename,“typename” 必须是 “number”, “string”, “boolean” 或 “symbol”。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

3.3 instanceof 关键字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

3.4 自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

四、关于使用ts的一些建议

  1. 使用模块代替命名空间 (命名文件里使用了export字段后文件被认为模块文件,无法使用命名空间)
  2. 不要使用/// 命令
  3. 类型命名使用大驼峰
  4. 使用import引入类型时,使用type 字段
  5. 使用Record代替object 类型

五、探索

映射类型与此类型操作部分中的其他功能很好地配合:(我没看懂)

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

获取T中所有类型为函数的key组成的联合类型。

/**
 * @desc NonUndefined判断T是否为undefined
 */
type NonUndefined<T> = T extends undefined ? never : T;

/**
 * @desc 核心实现
 */
type FunctionKeys<T extends object> = {
  [K in keyof T]: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T];

/**
 * @example
 * type Eg = 'key2' | 'key3';
 */
type AType = {
    key1: string,
    key2: () => void,
    key3: Function,
};
type Eg = FunctionKeys<AType>;
  • 首先约束参数T类型为object
  • 通过映射类型K in keyof T遍历所有的key,先通过NonUndefined<T[K]>过滤T[K]为undefined | null的类型,不符合的返回never
  • 若T[K]为有效类型,则判断是否为Function类型,是的话返回K,否则never;此时可以得到的类型,例如:
  • 最后经过{省略}[keyof T]索引访问,取到的为值类型的联合类型never | key2 | key3,计算后就是key2 | key3;
enum drawEnum {
  residualPlots = '残差图',
  scalar = '标量图',
  streamtrace = '流线图',
  contour = '等值线图',
  linearGraph = '曲线图'
}

export type drawType = keyof typeof drawEnum;
export type drawText = `${drawEnum}`;