基础
extends 关键词
extends有三种用途:继承、泛型约束、分配(条件类型)
1. 继承/扩展
一方面和 js 作用一致,用在 class 上。
另一方面,也可以继承类型,只有interface可用
interface Person { name: string }
interface Child extends Person { age: number }
// interface Child = { name: string; age: number }
2. 泛型约束
在泛型中,有时需要约束类型参数,可以借助extends来完成,比如
// 希望传入的参数是个函数
type Type<T extends (props: any) => void>= { info: T };
const obj: Type<() => void> = {info: function(){}}
// obj中的person属性必须包含name属性
interface Type<T extends { name: string }> {
person: T;
}
const obj: Type<{ name: string; age: number }> = {
person: { name: 'hxy', age: 25 },
};
3. 分配(条件类型)
extends还可以用来判断一个类型是否可以分配给另一个类型,先看例子
type A = { name: string };
type B = { name: string };
type C = Child extends Person ? string : number;
// type C = string
和三元运算一样,第三行的意思是,A类型 是否可以分配给 B类型?是的话 C 为string,否则 C 为number。这里的 A 和 B 类型一致,当然可以分配。
这里就可以明白上面第二条的泛型约束 T1 extends T2,意思就是 T1 需要可以分配给 T2。
需要强调的是,**A 可以分配给 B,不等于 A 是 B 的子集。**再看例子
type A = { name: string; age: number };
type B = { name: string };
type C = A extends B ? string : number;
// type C = string
这里的 A 显然不是 B 的子集,但条件判断依然成立(依然可以分配)。 这样的错误判断是因为用集合论的思想来理解类型系统了,即如果一个集合 A 的所有元素在集合 B 中都存在,则 A 是 B 的子集。 在类型系统中,先说结论:**两个类型间,更具体的类型是子类型,更宽泛的类型是父类型,子类型可以分配(赋值)给父类型。**举两个例子来加深理解:
- 上面的例子,A 的属性更多,它更具体, B 的属性少,它就更宽泛,所以 A 是子类型,所以 A 可以分配给 B。和类来类比理解,比如
class A extends B {},显然 A 是子类 B 是基类,A 当然可以分配给 B,用结论来解释的话,A 可能在 B 的基础上扩展属性变得更具体。 type Type = 1 | 2 extends 1 ? 'yes' : 'no'。Type 是 no,因为1|2有两个选项更宽泛(父),1只有一个选项更具体(子)。
有了 extends 可以用作分配的基础知识,下面来详细看看条件类型
条件类型
从上文明白条件类型的基本用法,下面看一个搭配泛型使用的例子
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 函数基于输入值的类型不同而返回不同的类型。可以看到这里只有两个类型,就需要重载三次,如果有一天要加一个类型,重载的次数会呈指数增加。
写js代码时我们通常会使用三元运算符减少一些if/switch语句,上面的例子同样可以用条件类型优化
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 = createLabel("typescript");
// let a: NameLabel
let b = createLabel(2.8);
// let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
// let c: NameLabel | IdLabel
分发条件类型
type Example1 = 'x' extends 'x' ? string[] : number[];
// type Example1 = string[]
type Example2 = 'x' | 'y' extends 'x' ? string[] : number[];
//type Example2 = number[]
type ToArray<T> = T extends 'x' ? string[] : number[];
type Example3 = ToArray<'x' | 'y'>; // ?
直觉告诉我Example3是number[],实际上是string[] | number[],因为若给 ToArray 传入一个联合类型,这个条件类型会被分发到联合类型的每个成员
ToArray<'x' | 'y'>;
// 相当于
ToArray<'x'> | ToArray<'y'>;
// 结果显然就是 string[] | number[]
通常这是我们期望的行为,如果要避免这种行为,可以用方括号包裹 extends 关键字的每一部分。
type ToArray<T> = [T] extends ['x'] ? string[] : number[];
type Example3 = ToArray<'x' | 'y'>;
// type Example3 = number[];
infer 关键词
条件类型的基本语法 T extends U ? X : Y;
如果占位符类型 U是一个可以被分解成几个部分的类型,比如数组类型、元组类型、函数类型、字符串字面量类型等。这时候可以通过 infer 来推断 U 类型中某个部分的类型。
infer使用限制:只能被用在条件类型中,只能在**true**分支使用。
// 1. 推断数组(或者元组)的类型
type ArrayItem<T> = T extends (infer U)[] ? U : never;
type Example1 = Type1<string[]>
// type Example1 = string
type Example2 = ArrayItem<[number, string]>
// type Example2 = string | number
// 2. 推断数组(或者元组)第一个元素的类型(最后一个写法类似)
type First<T extends unknown[]> = T extends [infer P, ...infer _] ? P : never
type Example3 = First<[3, 2, 1]>
// type Example3 = 3
// 3. 推断函数类型的参数 和 推断函数类型的返回值,内置了Parameters 和 ReturnType
readonly 关键词
可以在数组、元组、接口、对象类型上使用readonly。 类里的字段加一个 readonly 前缀修饰符,会阻止在构造函数之外的赋值。 as const也会把字段转为只读,一般用在数组和对象上。
type T1 = readonly [number, string] // 元祖
type T2 = readonly string[] // 数组
type T3 = {readonly name: string}
type T4 = readonly string // ❌ 不允许这样写
class Person {
readonly name: string = "hxy";
// 只允许在constructor里修改
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
}
const arr = [1,2] as const
// const arr: readonly [1, 2]
const arr = {name: 'hxy'} as const
// const arr: { readonly name: "hxy"; }
keyof 操作符
TS 中keyof操作符只能用于类型,不能用于值,Class 除外,因为在 TS 中 Class 本身也可作为类型。往往keyof需要搭配typeof使用。
计算属性名
因为计算属性名的类型只能是string、number、symbol或any,所以当不确定键的类型时,keyof通常会返回string|number|symbol
const key1 = 1;
const key2 = 'string';
const key3 = Symbol();
const Obj = {
[key1]: 1,
[key2]: 2,
[key3]: 3,
};
type T = keyof typeof Obj;
// type T = 1 | "string" | typeof key3
// key3 只可能是 string、number、symbol或any,所以typeof key3等于string|number|symbol
所以如下代码会报错,因为没有约束T的类型
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string = k;
// error 不能将类型“string | number | symbol”分配给类型“string”.
}
// 当然是可以使用Extract解决
function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
var name: string = k;
}
用于Class
keyof可以直接用在class上,但要注意,静态属性不会被keyof获取,除此之外所有实例属性都会被获取。
class Base {
name: string = 'xxx';
static age = 'ccc';
fun = () => {};
fun2() {}
}
type T = keyof Base;
const a: T = 'age';// error
const a: T = 'name'; // ok
typeof 操作符
TS 限制了可以使用 typeof 的表达式的种类,只有对标识符(比如变量名)或者他们的属性使用,下面的例子是典型的错误
const Type = typeof fun();
看起来像是获取函数返回值的类型,但是函数调用不是标识符或属性,显然不能使用(可以使用ReturnType实现)
**typeof**部分使用示例:
// ----用于对象
const person = { name: "hxy", age: 24 }
type My = typeof person;
// type My = { name: string; age: number }
// ----用于函数,对函数使用是很常用的用法,比如使用 Parameters 和 ReturnType 内置类型就需要对函数用typeof
function fun<Type>(arg: Type): Type {
return arg;
}
type Fun = typeof fun;
// type Fun = <Type>(arg: Type) => Type
// ----用于枚举
enum ActionType {
ADD = 'add',
DELETE = 'delete',
}
type T1 = typeof ActionType;
// type T = {
// ADD: ActionType.ADD;
// DELETE: ActionType.DELETE;
// };
const a: T1 = { ADD: ActionType.ADD, DELETE: ActionType.DELETE };
// 看起来毫无意义,一般可以搭配keyof获取枚举的键
type T2 = keyof typeof ActionType
// type T2 = "ADD" | "DELETE"
接口interface
命名对象类型的另一种方式,下面主要来看接口和类型别名的区别:
- 接口可以合并,类型别名不可以。
接口重复定义时会合并属性,但是如果出现定义重复属性,重复属性的类型需要一致,否则会报错。
重复定义类型别名会报错,类型别名如果想要扩展,需要使用交叉类型的方式,交叉时出现重复属性不会报错,但是可能会成为unknown类型。
interface Person { name: string }
interface Person { age: number }
// interface Person {
// name: string;
// age: number;
// }
interface Person { name: number }
// error 后续属性声明必须属于同一类型。属性“name”的类型必须为“string”,但此处却为类型“number”。ts(2717)
type Animal = { name: string }
type Animal = { age: number }
// error 标识符“Animal”重复。ts(2300)
// 交叉类型扩展
type NewAnimal = Animal & {
age: number
}
// name变为never类型
type NewAnimal = Animal & {
name: number
}
- 接口可以使用extends,类型别名不可以
使用extends也是一种扩展属性方式,不能用在类型别名上,类型别名还是使用交叉类型来扩展。
- 接口只能定义对象类型,类型别名没有限制
索引访问类型
用于获取一个类型上的特定属性,要获取属性的对象和索引名都需要是一个类型
type Person = { age: number; name: string };
const key1 = 'age';
type key2 = 'age';
type AgeType1 = Person[key1]; // error key1不是类型
type AgeType2 = Person[key2]; // ok, type AgeType2 = number
// 我们经常会将联合类型等作为索引名
type keyType1 = Person[keyof Person]; // type keyType1 = string | number
type keyType2 = Person['age' | 'name']; // type keyType2 = string | number
用于数组
结合typeof可以通过索引number获取到数组字面量的类型
const MyArray = [{ name: 'hxy', age: 24 }];
// 从左往右运算,先typeof再索引访问
type My = typeof MyArray[number];
// type My = {
// name: string;
// age: number;
// }
可以这么理解,数组本身就是一个键为数字的对象,所以可以通过number索引获取数组字面量类型
// 数组是一个类似于下面的实现
interface Arr {
[index: number]: any;
}
// 准确的实现是
interface Array<T> {
at(index: number): T | undefined;
}
当然要注意,我们一般不能通过string索引获取对象的属性
type Person = { age: number; name: string };
type keyType = Person[string]; // 类型“Person”没有匹配的类型“string”的索引签名
// 当然如果这样定义就可以了
type Person = { [key: string]: number };
type keyType = Person[string]; // type keyType = number
映射类型
一般会和keyof搭配使用。先来看一个最基本的例子,将 Type 所有属性设置为布尔类型
type BecomeBoolean<T> = {
[Property in keyof T]: boolean;
};
映射修饰符
在映射类型中,可能会用到readonly和?修饰符。可以通过+``-来增加或去掉修饰符,没写相当于+
type BecomeReadable<T> = {
-readonly [Property in keyof T]: T[Property];
};
type Type = {
readonly id: string;
readonly name: string;
};
type ReadableType = BecomeReadable<Type>
// type ReadableType = {
// id: string;
// name: string;
// }
利用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>;
这个Getters的实现利用到了Capitalize,它用于将一个字符串模版字面量类型的首字母转为大写。
在as后面跟的其实可以是一个类型,上面的例子相当于跟的是字符串模版字面量类型。
type BecomeString<T> = {
[Property in keyof T as string]: T[Property];
};
interface Person {
age: number;
name: string;
}
type NewType = BecomeString<Person>;
// type NewType = {
// [x: string]: string | number;
// }
在联合类型中,never会被过滤掉,所以我们就可以利用never来过滤对象类型中的某个属性:
type RemoveKind<T> = {
[Property in keyof T as Exclude<Property, "kind">]: T[Property]
};
interface Type {
kind: number;
name: string;
}
type NoKind = RemoveKind<Type>;
// type NoKind = {
// name: string;
// }
Exclude<Property, "kind">,可以分配给'kind'的类型会返回never,所以在联合类型中会被过滤掉。
在映射类型中,我们当然不用局限于使用keyof,遍历的是联合类型就可以。也不用局限于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>
// type Config = {
// square: (event: SquareEvent) => void;
// circle: (event: CircleEvent) => void;
// }
泛型
Generics,泛型就是被用来描述两个值的类型之间的对应关系,我一般会用函数参数的概念来理解,泛型就是传入一个类型参数。
泛型约束
泛型可以被约束,上文extends关键词中有提到。
泛型函数
函数中有两个或以上的类型有关联,比如多个参数或者参数和返回值的类型有关联,考虑使用泛型函数。
function firstEle<Type>(arg: Type[]): Type {
return arg[0];
}
// 当然也可以这么写
type T = <Type>(arg: Type[]) => Type;
const firstEle: T = arg => arg[0];
// 可以不传入类型参数,ts会根据参数推导出泛型的类型
firstEle([1, 2, 3]);
// function firstEle<number>(arg: number[]): number
// 当然可以自己传,只是没必要
firstEle<number>([1, 2, 3]);
泛型对象
需要注意的是,和泛型函数不同,泛型对象必须传入类型参数,无法通过属性的类型推导,不过可以有默认值。
interface Box<Type> {
contents: Type;
}
const a: Box<number> = {contents: 1}
interface Type<T = string>{
name: T // T默认为string
}
泛型类
泛型类不一定要传入类型参数,像泛型函数一样会推导。 一个类的类型有两部分:静态部分和实例部分。泛型类仅仅对实例部分生效,所以当使用类的时候,注意静态成员并不能使用类型参数。
class Person<Type1, Type2> {
name: Type1;
age: Type2;
getInfo: (x: Type1, y: Type2) => Type1;
static field: Type1;// 静态成员不能引用类类型参数
constructor(name: Type1, age: Type2) {
this.name = name;
this.age = age;
}
}
let per = new Person('hxy', 24);
per.name = 'hxy';
per.getInfo = function (x, y) {
return x + '' + y;
};
使用泛型的注意点
- 可以不使用泛型约束的情况下,尽量直接使用类型参数
- 尽可能少的设立类型参数,能通过已有类型参数推导出来的,就不要定义新的
- 泛型是用来关联多个值的类型关系的,定义单个值的类型不需要使用泛型
元组类型
Tuples,type T = [string, number],记录一些元组的注意点:
- 在元组类型中,也可以写可选属性,但可选元素必须在最后面,而且也会影响类型的
length。
type T = [number, number, number?];
function fun(arg: T) {
const [x, y, z] = arg;
// const z: number | undefined
console.log(arg.length);
// (property) length: 2 | 3
}
- 可以使用剩余元素语法,不需要写在最后面,但必须是
Array/Tuple类型,并且有剩余元素的元组不会设置length
type T1 = [string, number, ...boolean[]];
type T2 = [string, ...boolean[], number];
type T3 = [...boolean[], string, number];
const a: T1 = ['1', 2, true];
a.length
// (property) length: number
- 可以使用
readonly
type T = readonly [string, number]
类
构造函数
和普通函数一样,构造函数可以给参数加类型注解、参数默认值、重载等,但也有不同:
- 构造函数上不能使用泛型
- 构造函数不能有返回值类型注解,因为总是会返回实例类型
TS 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性,这些就被称为参数属性。我们可以在参数前面加可见性修饰符或者readonly来创建参数属性,最后这些类属性字段也会得到这些修饰符
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
Getters/Setters
- 如果
get存在而set不存在,属性会被自动设置为readonly - 如果
set参数的类型没有指定,它会被推断为get的返回类型 set的参数类型不一定需要和get的返回值相同
implements
类使用implements意味着需要实现一个接口(对象类型即可)
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
// 类“Ball”错误实现接口“Pingable”。
// 类型 "Ball" 中缺少属性 "ping",但类型 "Pingable" 中需要该属性。
pong() {
console.log("pong!");
}
}
可以同时实现多个接口,但多个接口的同一个属性如果类型不一致,会出现冲突
type Person = {
name: string;
}
type Child = {
name: number;
}
class Stu implements Person, Child {
// 类型“Stu”中的属性“name”不可分配给基类型“Person”中的同一属性。
// 不能将类型“number”分配给类型“string”。
name = 1
}
成员可见性
- public:没有限制,默认值
- protected:仅对子类可见
- private:只在类内部可见,子类也不可见
需要注意的是,可见性的这些修饰符仅在类型检查中生效,转为 JS 后会失效。
静态成员
- 使用
static修饰符表示某成员是静态成员,和实例成员有区别,静态类型只能在类本身访问。 - 静态属性可以使用可见性修饰符。
- 类是函数,函数本身有一些属性比如
name,那么设置静态属性不能起这些特殊名字,比如name、length、call等。 - 类使用泛型时,静态成员不可用类型参数。
抽象类和成员
使用abstract修饰符表示是抽象的。抽象成员必须存在于抽象类中,抽象意味着不提供实现,并且抽象类也不能被实例化,它的作用是做为类的基类。
派生类必须要实现抽象类所有标识的属性。
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
class Derived extends Base {
getName() {
return "world";
}
}
内置类型
Partial 和 Required
Partial:将 T 中所有属性变为可选的 Required:将 T 中所有属性变成必选的
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
Readonly
将 T 中所有属性变为只读的
type Readonly<T> = { readonly [P in keyof T]: T[P] };
Pick
从已有对象类型中选取给定的属性及其类型,然后构建一个新的对象类型。 T 表示源对象类型,类型参数 K 提供了待选取的属性名类型,它必须为类型 T 中存在的属性。
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// 例
type T1 = { name: string; age: number; gender: string };
type T2 = Pick<T1, 'age'|'name'>
// type T1 = { age: number; name: string; }
Extract 和 Exclude
Extract:从T中提取可分配给U的类型 Exclude:从T中排出可分配给U的类型 下面的例子用到了上文提到的分发条件类型
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
// 例
type Type = '1' | '2' | 1 | 2;
type Example1 = Extract<Type, number>;
// type Example1 = 1 | 2
type Example2 = Exclude<Type, number>;
// type Example2 = '1' | '2'
Omit
构建一个新类型,排除T中拥有的K的属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 例
type Type = { name: string; age: number };
type Example = Omit<Type, 'name'>;
// type Example = { age: number; }
Record
构建一个新的对象类型,键的类型是 K (in K),值的类型是 T
type Record<K extends keyof any, T> = { [P in K]: T };
// 例
type keys = 'A' | 'B'
type T = Record<keys, number>
// type T = { A: number; B: number; }
ReturnType
获取函数的返回类型,借助条件类型和infer
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// 例
const add = (num1: number, num2: number) => num1 + num2;
type returnType = ReturnType<typeof add>;
// type returnType = number
Parameters
获取函数的参数的类型,返回的一定是元祖或数组类型。借助条件类型和infer
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
// 例
const add = (...args: number[]) => args.reduce((p, c) => p + c);
type Para = Parameters<typeof add>;
// type Para = number[]
const a: Para = [1, 2];
ReadonlyArray
描述数组的属性不可以被更改,和Array用法一样,并且ReadonlyArray<Type>也有简写形式readonly Type[]。
需要注意,Array和ReadonlyArray不能双向赋值
let x: readonly number[] = [2];
let y: number[] = [1];
x = y; // ok
y = x; // error: 类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"
字符串操作
- Uppercase:每个字符转为大写
- Lowercase:每个字符转为小写
- Capitalize:首字母转为大写
- Uncapitalize:首字母转为小写
以上类型都需要传入类型参数,一般会传入模版字面量类型,本身的要求就是要可分配给string类型。但我们不确定传入的类型是什么时可以通过交叉类型来限制。
type T = 1
type Type = Uppercase<string & T>