本文首发于个人博客
泛型
首先, 我们来写一个函数: loggerNum 函数, 这个函数的作用是 console.log 输入数字值, 然后将该值返回:
const loggerNum = (params: number) => {
console.log(params);
return params;
};
loggerNum(1);
// number
loggerNum('1');
// Error: 字符串不能分配给数字类型
假如说我们还需要打印字符串呢?
const loggerStr = (params: string) => {
console.log(params);
return params;
};
loggerStr('1');
// string
假如说我们还有布尔类型, 害, 是不是得继续声明一个函数来实现, 算了, 我使用 any:
const logger = (params: any) => {
console.log(params);
return params;
};
// logger(1);
// any
// logger('1');
// any
// logger(false);
// any
这种方式当然没有问题, 但是它失去了原有的类型以及类型检查(不到万不得已, 请不要使用 any), 所以我们需要一种捕捉参数类型的方式, 以便我们可以使用它来表示返回的内容.
function logger(params: Type): Type {
console.log(params);
return params;
}
我们现在给
logger
函数添加来一个类型变量Type
, 然后Type
可以捕获到提供的类型(比如说number
、string
...), 后面我们就可以使用该Type
, 在这里我们使用Type
作为参数和返回值, 所以 Ts 会检查其返回值是不是与Type
是一致的.logger(1);
// number
logger('1');
// string
logger(true);
// boolean
logger([1, 2, 3]);
// number[]
logger(1);
// 数字类型不能分配给字符串类型
我们也可以使用类型参数自动推断的方式, ts 编译器会根据我们传入的参数自动设置其类型.
logger(1);
// number
logger('1');
// string
logger(true);
// boolean
logger([1, 2, 3]);
// number[]
我们第一个简单的泛型函数就实现啦~.
泛型约束
我们再拿上面那个例子来说, 我们想打印一下参数的长度, 改一下代码:
function logger(params: Type): Type {
console.log(params.length);
// Property 'length' does not exist on type 'Type'.
return params;
}
报错了, 别慌张, 因为使用泛型, 我们此时不能访问它的任何属性, 所以这个时候报错是正常的, 做一个类比, 我们把之前的例子比喻成一个充电器, 什么充电器都可以, 只要是充电器就好了, 现在充电器加了约束(类型约束), 只能适配 type-c 充电器, 不是 type-c 充电器都不行.
这里也是一样的, 我们希望参数把类型做一个限制, 至少具有
length
属性的类型才可以传入, 所以来看看代码.function logger(params: Type): Type {
console.log(params.length);
return params;
}
Type extends {length:number}
就是做了一个类型约束, 只有具有length
属性的对象才可以传入.logger({ length: 0 });
logger([1, 2, 3]);
logger(1);
// number 不能分配给 { length:number }
在泛型约束中使用类型参数
有了上面的基础, 我们再来实现一个方法
getProperty(obj, key)
返回对象中的指定 key 的 value.function getProperty(obj: Type, key: keyof Type) {
return obj[key];
}
keyof 后面会介绍默认类型参数
在 Js 中有默认参数, 如果没有传值时使用该默认值.
const inc = (count, step = 1) => count + step;
inc(1);
// 2
inc(1, 5);
// 6
而在 Ts 中有默认类型参数, 如果没有传入参数就使用默认类型参数, 上面
getProperty
的例子更新一下:function getProperty(obj: Type, key: Key) {
return obj[key];
// Key 不能当作Type的索引
}
成功的报错了,
Key
在不传入类型的时候,才会是默认的keyof Type
, 而如果Key
如果传入了number
、string
等类型时, Key
就会采用传入的类型:getProperty({ name: 'senlin', age: 18 }, false);
// getProperty(obj: Person, key: boolean)
getProperty({ name: 'senlin', age: 18 }, '444');
// getProperty(obj: Person, key: string)
所以我们需要对
Key
做一个参数类型约束:function getProperty(
obj: Type,
key: Key
) {
return obj[key];
}getProperty({ name: 'senlin', age: 18 }, false);
// Error: false不能分配给'name'|'age'
getProperty({ name: 'senlin', age: 18 }, '444');
// Error: '444'不能分配给'name'|'age'
getProperty({ name: 'senlin', age: 18 }, 'name');
// OK: 'senlin'
交叉类型 & 类型运算符 & 用于创建交叉类型:
type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
// "b" | "c"
type Intersection = A & B;
如果我们将类型 A 和类型 B 视为集合, 那么 A & B 就是两个集合的交集, 换句来说: 结果的成员是两个操作数的成员.
与 never、unknown 的爱恨情仇
type A = 'a' | 'b';
type D = A & never;
// never
type E = A & unknown;
// 'a' | 'b'
如果把 ts 的类型当作一个集合来看的话, unknown 相当于集合中的全集, 它是一个顶部类型:
- 空集(never)和其他集合(A)做交集(交叉类型) = 空集(never).
- 全集(unknown)和其他集合(A)做交集(交叉类型) = 其他类型.
function wrapToArray(params: number) {
return [params];
}wrapToArray(1);
// number[]
wrapToArray([1]);
// number[] 不能分配给number
所以这个时候, 我们就需要采用联合类型, 期望参数可以传入数字和数字数组.
function wrapToArray(params: number | number[]) {
// 类型缩小
if (Array.isArray(params)) return params;
return [params];
}wrapToArray(1);
// number[]
wrapToArray([1]);
// number[]
注意这里的参数
number|number[]
, 意思就是允许传入数字和数字数组类型, 这里我们使用Array.isArray
来进行类型缩小, 如果是数组就直接返回, 如果不是数组就进行包裹一层返回.为什么要进行类型缩小? 因为 Ts 在使用过程中需要明确具体的类型(any 除外!), 当前类型是我们也可以使用number|number[]
, 并不清楚是number
类型还是number[]
类型, 所以需要使用Array.isArray
来将其缩小到number[]
类型.
|
来创建联合类型:type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
// "a" | "b" | "c" | "d"
type Union = A | B;
如果把 A 和 B 当作两个集合, 联合类型就是求两个集合的并集, 结果的成员是至少一个操作数的成员.
如果我们对一个对象类型进行
keyof
操作的时候, 也会得到联合类型:type Person = {
name: string;
age: number;
};
type PersonKeys = keyof Person;
// 'name' | 'age'
对象类型的联合
由于联合类型的每个成员都是至少一个组件类型的成员, 我们只能安全的访问所有组件类型共享的属性(A 行). 如果要访问其他属性, 我们需要一个类型保护(B 行):
type Person = {
name: string;
phone: string;
};
type Teacher = {
name: string;
project: string;
};
type Union = Person | Teacher;
function fn(params: Union) {
params.name;
// (A行) OK
// 报错: Property 'phone' does not exist on type 'Union'.
params.phone;
// error// (B行) type guard
if ('project' in params) {
// Teacher
params;
// string
params.project;
} // (C行) type guard
if ('phone' in params) {
// Person
params;
// string
params.phone;
}
}
联合类型与 never、unknown 的爱恨情仇
type A = 'a' | 'b';
type B = A | never;
// 1: 'a' | 'b'
type C = A | unknown;
// 2: unknown
如果把 ts 的类型当作一个集合来看的话, never 相当于集合中的空集:
- 空集(never)和其他集合(A)做并集(类型联合) = 其他集合(A).
- 全集(unknown)和其他集合(A)做并集(类型联合) = 全集(unknown).
所以很多时候, 我们在使用联合类型、条件类型时会和never
一起使用.
条件类型
基本格式:Type2 extends Type1 ? ThenType : ElseType
如果
Type2
类型能够分配给Type1
类型的话, 返回ThenType
否则就是ElseType
类型, 相当于是类型版本的三目运算符.例子一: 仅包装带有 length 属性的值.
在下面例子中, 如果类型可以分配给
{length:number}
的时候把它包装成一个元素的元祖:type Wrap = T extends { length: number } ? [T] : T;
type A = Wrap;
// [string]
type B = Wrap;
// number
分配性检查
我们可以使用条件类型来判断分配性检查:
type IsAssignableTo = A extends B ? true : false;
// true
type Result1 = IsAssignableTo<123, number>;
条件类型是分布式的
条件类型是分布式的:将条件类型应用 C 到联合类型与应用到每个组件的 U 联合相同。这是一个例子:CU
type Wrap = T extends { length: number } ? [T] : T;
type A1 = Wrap;
// 等同于
type A2 = Wrap | Wrap | Wrap;
// 等同于
type A3 = number | boolean | [string];
用一个不太恰当的比喻就是: 乘法分配律
a*(b+c) = a*b + a*c
.对于分布式条件类型, 可以使用 never 忽略某一个结果
再来看一下下面这段代码:
type Wrap = T extends { length: number } ? [T] : never;
type A1 = Wrap;
// 等同于
type A2 = Wrap | Wrap | Wrap | Wrap;
// 等同于
type A3 = never | [string] | never | [number[]];
// 等同于
type A4 = [string] | [number[]];
递归条件类型
在 Js 中, 可以看到在任意级别展平和构建容器类型的函数是很常见的. 比如说
.then()
返回的Promise
会进行展开, 直到找到一个不是"promise-like"的值, 然后将该值传递给回调,【一文读懂 TypeScript 中的范型是如何计算的】再比如说, 我们想编写一个类型来获取嵌套数组的元素类型 deepFlatten.
type ElementType = T extends ReadonlyArray ? ElementType : T;
declare function deepFlatten(
x: T
): ElementType[];
deepFlatten([1, 2, 3]);
// number[]
deepFlatten([[1], [2, 3]]);
// number[]
deepFlatten([[1], [[2]], [[[3]]]]);
// number[]
判断元素
T
是否可以分配给ReadonlyArray
, 如果可以分配就进行递归操作ElementType
, 直到不能分配为止.我们还可以编写一个
Awaited
类型来展开Promise
获取最后的类型.type Awaited = T extends PromiseLike ? Awaited : T;
type P1 = Awaited>;
// string
type P2 = Awaited>>;
// string
type P3 = Awaited | undefined>>>;
// string | number | undefined
infer
在 extends 条件类型的子句中, 现在可以有 infer 引入要推断的类型变量的声明. 这种推断的类型变量可以在条件类型的真实分支中引用. 同一个类型变量可以有多个 infer 位置.大白话就是: 在 extends 条件类型中, 在真实分支中, 声明一个类型变量先占住这个坑位, 具体类型由传入参数的类型决定.
所以 infer 关键字允许我们从条件类型中推断出另外一个类型, 比如说:
type UnpackArrayType = T extends (infer R)[] ? R : T;
type T1 = UnpackArrayType;
// number
type T2 = UnpackArrayType;
// string
UnpackArrayType
是一个条件类型, 如果T
可以分配给(infer R)[]
, 就返回这个R
, 否则就返回T
.T1:
UnpackArrayType
中的条件是正确的, 声明一个类型变量R
占住这个坑位, 因为number[]
与(infer R)[]
匹配, 所以类型变量R
就是传入的类型number
, 然后作为推断过程的结果返回. infer
的作用是告诉编译器在UnpackArrayType
范围内声明了一个新的类型变量 R, 然后具体类型由传入的类型进行填充 .T2:
UnpackArrayType
中的条件是不成立的, 因为string
并不能分配给(infer R)[]
, 所以直接就返回T
, 也就是返回对应的string
类型.ReturnType
利用
infer
我们就可以实现ReturnType
, 返回一个函数的结果类型:type MyReturnType = T extends (...args: any) => infer R ? R : never;
MyReturnType<() => string> // string
MyReturnType // never
如果
T
可以分配给一个函数的话, 我们声明一个类型变量R
, 占住这个坑位, 然后在我们传过去的类型是string
, 所以这个R
的类型就是string
.infer 只能在 extends 的真实分支上面引用:
type MyReturnType = T extends (...args: any) => infer R ? never : R;
// Error: 找不到名称R
infer 不能在约束子句中对常规类型参数使用声明:
type MyReturnType infer R> = R;
// Error: 并不支持
如果想在类型参数上面使用的话, 不妨试试这种方式:
type MyReturnType infer R ? R : never> = L;
// OK
MyReturnType<() => string> // string
对 key 进行重命名
interface ApiData {
'maps:person': string;
'maps:age': boolean;
address: string;
}
type RemoveMapsFromObj = {
[P in keyof T as RemoveMaps]: T[P];
};
type RemoveMaps = T extends `maps:${infer S}` ? S : T;
type RemovedPerson = RemoveMapsFromObj;
映射类型 映射类型通过循环某一组 key 来生成一个对象:
type Obj = {
[K in 'name' | 'address']: string;
};
// { name:string;
address:string }
如果我们自己要实现一个
Partial
呢?type MyPartial = {
[P in keyof T]+?: T[P];
};
type Person = {
name: string;
age: number;
};
type PartialPerson = MyPartial;
// { name?:string;
age?:number }
我们来一点点分析一下:
type PartialPerson = {
[P in keyof Person]+?: T[P];
};
然后分解成
keyof Person
:type PartialPerson = {
[P in 'name' | 'age']+?: T[P];
};
我们再来将分解出来的 key 进行下一步操作:
type PartialPerson = {
['name']+?: T['name'];
['age']+?: T['age'];
}
+?
的意思就是添加一个?
符号, 也就是可选项属性的意思, 所以最后的结果:type PartialPerson = {
name?: string;
age?: string;
};
映射类型中 key 的重新映射
映射类型只能使用我们提供的键生成新的对象类型, 但是很多时候我们希望根据输入创建一个新的 key 或者过滤掉某一些 key.
在上面的例子中, 不想要人知道我的年龄和手机号, 在
Person
中去掉这两个字段的声明.interface Person {
name: string;
age: number;
address: string;
phone: string;
}
type RemoveSecretProps = {
[P in keyof T as P extends 'age' | 'phone' ? never : P]: T[P];
};
// 使用Exclude进行简化
type RemoveSecretProps2 = {
[P in keyof T as Exclude]: T[P];
};
type SecretPerson = RemoveSecretProps;
/*
{
name: string
address: string
}
*/
它的运行流程:
type RemoveSecretProps = {
['name' extends 'age' | 'phone' ? never : 'name']: Person['name']
['address' extends 'age' | 'phone' ? never : 'address']: Person['address']
['age' extends 'age' | 'phone' ? never : 'age']: Person['age']
['phone' extends 'age' | 'phone' ? never : 'phone']: Person['phone']
}// 等同于
进一步:
type RemoveSecretProps = {
['name']: Person['name'];
['address']: Person['address'];
};
最后:
type RemoveSecretProps = {
name: string;
address: string;
};
当 as 子句中指定的类型解析为
never
, 不会为该键生成任何属性. 因此, as 子句可以用作过滤器.模版文字类型 模版文字类型是建立在字符串文字类型之后, 并且可以通过联合扩展出其他字符串, 允许我们对需要一组特定字符串的函数和 API 进行建模.
它的语法和 Js 中的模版字符串一样, 但是是用于类型:
type World = 'world';
type Hi = `hello ${World}`;
// hello worldfunction setAlignment(location: 'top' | 'middle' | 'bottom') {}
setAlignment('middel');
// middel 不能分配给 top | middle | bottom
- 如果模版文字类型是联合类型的话, 占位符中的联合类型分布在模板文字类型上. 例如
[${A|B|C}]
解析为[${A}]
|[${B}]
|[${C}]
. 多个占位符中的联合类型解析为叉积。例如[${A|B},${C|D}]
解析为[${A},${C}]
|[${A},${D}]
|[${B},${C}]
|[${B},${D}]
. - 占位符中的
string
、number
、boolean
和bigint
文字类型会导致占位符被文字类型的字符串表示形式替换。例如[${'abc'}]
解析为[abc]
和[${42}]
解析为[42]
. - 占位符中的任何一种类型
any
、string
、number
、boolean
和bigint
都会导致模板文字解析为string
类型 。 - 占位符中的类型
never
类型导致模板文字解析为never
.
type EventName = `${T}Changed`;
type Concat = `${S1}${S2}`;
type ToString = `${T}`;
type T0 = EventName<'foo'>;
// 'fooChanged'
type T1 = EventName;
// never
type T2 = EventName<'foo' | 'bar' | 'baz'>;
// 'fooChanged' | 'barChanged' | 'bazChanged'
type T3 = Concat<'Hello', 'World'>;
// 'HelloWorld'
type T4 = `${'top' | 'bottom'}-${'left' | 'right'}`;
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
type T5 = ToString<'abc' | 42 | true | -1234n>;
// 'abc' | '42' | 'true' | '-1234'
请注意, 联合类型的交叉积分布可能会迅速升级为非常大且成本高昂的类型. 另请注意, 联合类型限制为少于 100000 个成分, 以下将导致错误:
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`;
// Error
因为
Zip
是 0-9 的交叉积 > 100000, 所以会报错.type Digit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`;
// OK
文字模版类型来生成属性的 change 的方法
type OnPropsChangeMethod = {
[K in keyof T & string as `on${Capitalize}Change`]: (value: T[K]) => void;
};
declare function makeWatchObject(obj: T): T & OnPropsChangeMethod;
const person = makeWatchObject({
name: 'senlin',
age: 18,
});
person.onNameChange = (name) => {
console.log('newName:', name);
};
person.onAgeChange = (age) => {
console.log('newAge:', age);
};
文字模版类型实现一个简单的字符串模版, 来实现值的类型提示.
type Placeholder = S extends `${string}{${infer P}}${infer Rest}`
? P | Placeholder
: never;
declare function format(
template: S,
args: Record, unknown>
): string;
const f = format(`name: {name} , age: {age}`, { name: 'senlin', age: 18 });
// format(string, { name: unknown, age: unknown})
其他操作符号 索引类型查询运算符 keyof
在上面的例子中, 我们已经使用过
keyof
关键字, 在这里, 我们再来看看keyof
, 它可以列出对象的属性 key.type Person = {
name: 'senlin';
age: 18;
};
type PersonKeys = keyof Person;
// 'name' | 'age'
如果将
keyof
用于数组, 你会发现结果出乎意料:type ArrayKeys = keyof ['a', 'b', 'c'];
// number | '0' | '1' | '2' | 'length' | 'pop' | 'push' ...
结果:
- 元组元素的索引,作为字符串:"0" | "1" | "2"
- number 索引属性的类型
- length
- Array 方法的方法
never
:type ObjKeys = keyof {};
// never
这是 keyof 处理交叉类型和联合类型的方式:
type A = { a: number;
common: string };
type B = { b: number;
common: string };
type Result1 = keyof (A & B);
// 'a' | 'b' | 'common'
type Result2 = keyof A | keyof B;
// 'a' | 'b' | 'common'type Result3 = keyof (A | B);
// 'common'
type Result4 = keyof A & keyof B;
// 'common'
类型查询运算符 typeof
typeof 是将获取值转化为 ts 类型.
const https://www.it610.com/article/value = 'value';
type Value = https://www.it610.com/article/typeof value;
//'value'
第一个值
value
是 value 变量的值, 第二个value
是 value 变量的类型.const add = (a: number, b: number) => a + b;
type Add = typeof add;
// (a:number, b:number) => number
索引访问运算符 T[K]
索引访问运算符返回其键可分配给 T[K]的所有属性的类型, 也称为查找类型.
type Person = {
name: 'senlin';
age: 18;
};
type Name = Person['name'];
// 'senlin'
type Age = Person['age'];
// 18
type NameAndAge = Person['name' | 'age'];
// 'senlin' | 18;
[]
中的类型必须是Person
的属性键(由keyof
计算得出), 但是如果类型添加了索引签名的话, 我们就可使用索引类型:string 来读取了.type Obj = {
[key: string]: string;
};
type ObjKeys = keyof Obj;
// string | number
KeysOfObj 包括类型 number, 这是因为: JavaScript 在索引对象时将数字转换为字符串:
在读取对象属性时[..]使用数字索引时, JavaScript 实际上会在索引到对象之前将其转换为字符串. 这意味着使用 100(number 类型)进行索引与使用"100"(string 类型)进行索引是一回事, 因此两者需要保持一致.
const abc = {
1: 'one',
};
console.log(abc[1] === abc['1']);
// true
元组类型也支持索引访问:
type Tuple = ['a', 'b', 'c', 'd'];
// "a" | "b"
type Elements = Tuple[0 | 1];
括号运算符也是分布式的:
type MyType = { prop: 1 } | { prop: 2 } | { prop: 3 };
// 1 | 2 | 3
type Result1 = MyType['prop'];
// 等同于
type Result2 = { prop: 1 }['prop'] | { prop: 2 }['prop'] | { prop: 3 }['prop'];
// 所以就是 1 | 2 | 3
类型体操 使用 Hook + 泛型可以写出一个公共的 hook, 这里就展示一个简单通用的 api hook.
type RequestResult = {
data: T | null;
error: unknown;
abort: () => void;
};
function useFetch(url: string, options?: RequestInit): RequestResult {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
const [abort, setAbort] = React.useState<() => void>(() => {});
React.useEffect(() => {
const fetchData = https://www.it610.com/article/async () => {
try {
const abortController = new AbortController();
const signal = abortController.signal;
setAbort(abortController.abort);
const res = await fetch(url, { ...options, signal });
const json = (await res.json()) as T;
setData(json);
} catch (error) {
setError(error);
}
};
fetchData();
return () => {
abort();
};
}, []);
return { data, error, abort } as const;
}const { data, error } = useFetch<{ login: string;
id: number }>(
'https://api.github.com/users/itsuki0927'
);
/*
{
login: string
id: number
}
*/
url 解析成一个对象
需要解析 url 上的搜索参数时, 发现要么没有类型, 要么就是自己解析出来去指定某一个类型, 能不能通过 Ts 就完成这一层的参数类型解析呢?
type Convert = T extends `${infer Key}=${infer _}` ? Key : T;
type ParseSearchParameters<
T,
L = T extends `?${infer U}` ? U : T
> = L extends `${infer A}&${infer B}`
? {
[P in Convert>]: string;
}
: {
[P in Convert]: string;
};
const url = '?name=senlin&age=18&address=hunan';
declare function parseSearchParameters(
params: T
): ParseSearchParameters;
parseSearchParameters(url);
/*
{
name: string
age: string
address: string
}
*/
什么都不需要做, 它根据你的搜索格式自动给你转化成了对应的类型.
Join、Split
模板文字类型可以与递归条件类型组合以编写
Join
和 Split
迭代重复模式的类型.type Join = T['length'] extends 1 ? `${T[0]}` : T extends [infer A extends string, ...infer B extends string[]] ?`${A}${U}${Join}` : ''type Split = T extends '' ? [] : T extends `${infer A}${U}${infer B}` ? [A, ...Split] : [T]type A1 = Join extends never
? never
: `${P}Async`]: Promisify;
};
type FS = {
version: string;
open(path: string, cb: (err: any, n: number) => void): void;
read(fd: string, cb: (err: any, name: string) => void): void;
};
declare const fs: FS;
declare const fsp: PromisifyObject;
// (string) => Promise
fsp.openAsync('path').then((n) => {
console.log(n.toFixed(2));
});
相关资料 参考资料
- 映射类型的 key 重新映射和模版文字类型
- infer 的解释
- 递归条件类型
- keyof 解释
- 集合、交叉类型、联合类型
- 交叉类型
- type-challenges
- ts-toolbelt
- utility-types
- SimplyTyped
- Ts 官网