10 分钟速通 TS
TS 必知必会
用 20% 的知识解决 80% 的日常开发
type & interface
- type 类型别名。适用于定义复杂的类型组合,如联合类型和交叉类型。当需要表示一个值可能是多种类型之一,或者一个类型需要同时满足多种类型的特征时,类型别名非常方便
- interface 接口。更适合用于定义对象的形状,尤其是在面向对象编程或者定义 API 的返回值和参数类型时。当需要描述一个类应该实现的契约(如具有哪些方法和属性)时,接口是很好的选择
条件类型
extends 既可以用于继承,也可用于类型约束
namespace
用于防止类型重命名冲突,比如微信小程序提供了全局类型的 namespace
泛型
合理使用泛型是 ts 的精髓,能够更好的封装通用方法
学会泛型能够轻松阅读各类社区库的类型声明,良好的变量命名、类型声明很多时候可以替代文档
枚举 & 常量枚举
业务代码中常有关于枚举的判断,比如data.status===1
,这可能分散在各个项目文件,并且缺失语义
建议
如果不想在业务变动时一处处地逐个修改枚举判断,建议所有使用常量或枚举而不是硬编码
编译结果如下
'use strict';
var Status;
(function (Status) {
Status[(Status['SUCCESS'] = 0)] = 'SUCCESS';
Status[(Status['FAIL'] = 1)] = 'FAIL';
Status[(Status['PENDING'] = 2)] = 'PENDING';
})(Status || (Status = {}));
const _ConstStatus = {
SUCCESS: 0,
FAIL: 1,
PENDING: 2
};
console.log(Status.SUCCESS, 0 /* ConstStatus.SUCCESS */, _ConstStatus.SUCCESS);
Utility Types 实用工具类型
1. Partial<T>
定义和用途: Partial<T>
用于将一个类型 T
的所有属性变为可选的。这在处理对象类型时非常有用,比如在创建对象的部分属性更新函数时。 示例:
interface User {
name: string;
age: number;
email: string;
}
let partialUser: Partial<User> = {
name: 'John'
};
在这个例子中,partialUser
可以只包含 User 接口中部分属性,因为 Partial<User>
将 User
接口的所有属性都变成了可选属性。
2. Required<T>
定义和用途: 与 Partial<T>
相反,Required<T>
将类型 T
的所有可选属性变为必选属性。这有助于确保在某些场景下对象的完整性。 示例:
interface PartialProduct {
name?: string;
price?: number;
}
let requiredProduct: Required<PartialProduct> = {
name: 'Phone',
price: 599
};
这里 Required<PartialProduct>
强制要求 requiredProduct
必须包含 name
和 price
属性,不能有缺失。
3. Readonly<T>
定义和用途: Readonly<T>
用于创建一个新的类型,其所有属性都是只读的。这在需要确保对象属性不被意外修改的场景下非常有用,比如配置对象或者常量对象。 示例:
interface Settings {
theme: string;
fontSize: number;
}
let readonlySettings: Readonly<Settings> = {
theme: 'dark',
fontSize: 14
};
// readonlySettings.theme = 'light'; // 这行代码会报错,因为属性是只读的
4. Pick<T, K>
定义和用途: Pick<T, K>
从类型 T 中挑选出属性集 K 所指定的属性,创建一个新的类型。K 是一个联合类型,代表要选择的属性名。 示例:
interface Car {
brand: string;
model: string;
year: number;
color: string;
}
type CarInfo = Pick<Car, 'brand' | 'model'>;
let myCarInfo: CarInfo = {
brand: 'Toyota',
model: 'Corolla'
};
在这个例子中,CarInfo 类型只包含 Car 接口中的 brand 和 model 属性。
5. Omit<T, K>
定义和用途: 与 Pick<T, K>
相反,Omit<T, K>
从类型 T
中排除属性集 K
所指定的属性,生成一个新的类型。这在想要去除某些不需要的属性时很有用。 示例:
interface Person {
name: string;
age: number;
address: string;
phone: string;
}
type PersonWithoutAddress = Omit<Person, 'address'>;
let person: PersonWithoutAddress = {
name: 'Alice',
age: 30,
phone: '1234567890'
};
这里 PersonWithoutAddress
类型是从 Person
接口中排除了 address
属性后的新类型。
6. Record<K, T>
定义和用途: Record<K, T>
用于创建一个新的类型,其属性键的类型为 K
,属性值的类型为 T
。K
通常是一个字符串字面量类型或者联合类型,T
可以是任何类型。 示例:
type Colors = 'red' | 'green' | 'blue';
type ColorMap = Record<Colors, string>;
let colorMap: ColorMap = {
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF'
};
这个例子中,ColorMap
类型的对象以 Colors
联合类型中的字符串作为键,以 string
类型作为值。
7. Exclude<T, U>
定义和用途: Exclude<T, U>
用于从类型 T
中排除可以赋值给类型 U
的元素,返回剩余的类型。它主要用于处理联合类型。 示例:
type Numbers = 1 | 2 | 3 | 4 | 5;
type OddNumbers = Exclude<Numbers, 2 | 4>;
let oddNumber: OddNumbers = 1;
在这里,OddNumbers
是从 Numbers
联合类型中排除了偶数 2 和 4 后剩下的奇数类型。
8. Extract<T, U>
定义和用途: 与 Exclude<T, U>
相反,Extract<T, U>
从类型 T
中提取可以赋值给类型 U
的元素,生成一个新的类型。 示例:
type AllNumbers = 1 | 2 | 3 | 4 | 5;
type EvenNumbers = Extract<AllNumbers, 2 | 4>;
let evenNumber: EvenNumbers = 2;
此例中,EvenNumbers
是从 AllNumbers
联合类型中提取出偶数 2 和 4 后的类型。
业务应用实践
以上是静态类型校验,那么如何过渡到动态类型校验?
运行时数据校验
解决什么问题
- 后端服务互相调用,数据类型不确定
- 可能出现的属性空值
- 可能由类型导致的问题,比如 falsy 判断、字符串和数字相加
碰到的挑战
经过调研,选用了 zod 库作为方案实现的核心库
该库较为成熟,社区活跃,原生支持 TS,且能够完全对标 TS 语法,一份 schema 同时生成校验器和 TS 静态类型,使用简单、直观、轻便
const safeString = () =>
string().catch(ctx => {
log2wx(ctx.error.message);
try {
const data = JSON.stringify(ctx.input);
return data || '';
} catch (e) {
return '';
}
});
// coerce number 解析如'测试文本'这样的文本字符串,无法类型强转,会parse error
const safeNumber = () =>
number().catch(ctx => {
log2wx(ctx.error.message);
const { success, data } = coerce.number().safeParse(ctx.input);
if (success) return data;
else return 0;
});
const safeBoolean = () =>
boolean().catch(ctx => {
log2wx(ctx.error.message);
const { success, data } = coerce.boolean().safeParse(ctx.input);
if (success) return data;
else return false;
});
const safeArray = <T extends ZodTypeAny>(
schema: T,
params?: RawCreateParams & { filter?: (val: unknown[]) => unknown[] }
) => {
return array(schema).catch(ctx => {
log2wx(ctx.error.message);
if (Array.isArray(ctx.input)) {
return params?.filter ? params.filter(ctx.input) : ctx.input; // 避免混合类型数组报错 [{name:'test'},null]
} else {
return [];
}
});
};
type ObjectParams = ZodObject<ZodRawShape>;
const safeObject = <
T extends
| ObjectParams
| ZodDefault<ObjectParams>
| ZodNullable<ObjectParams>
| ZodDefault<ZodNullable<ObjectParams>>
>(
schema: T
) => {
return schema.catch((ctx: { error: ZodError }) => {
log2wx(ctx.error.message);
return {};
}) as ZodCatch<typeof schema>;
};
const safeLiteral = <T extends Primitive>(schema?: T) =>
literal(schema).catch(ctx => {
log2wx(ctx.error.message);
return schema;
});
const safeNativeEnum = <T extends EnumLike>(schema: T) =>
nativeEnum(schema).catch(ctx => {
log2wx(ctx.error.message);
return Object.values(ctx.input)[0];
});
const safeNull = () =>
zodNull().catch(ctx => {
log2wx(ctx.error.message);
return null;
});
const safeUnion = <T extends Readonly<[ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]>>(schema: T) => {
return union(schema).catch(ctx => {
log2wx(ctx.error.message);
return '';
});
};
const createBaseResponseValidator = <Data extends ZodType, Meta extends ZodType>(
data?: Data,
meta?: Meta
) => {
return safeObject(
object({
code: number(),
msg: string(),
data: preprocess(val => val || null, data || safeNull()),
meta: preprocess(val => val || null, meta || safeNull())
}).partial({
meta: true,
msg: true,
data: true
})
);
};
import { createDetailValitor } from './validator'
class DetailService extends BaseServices {
constructor() {
super();
}
getDetail(query: { orderId: string }) {
return this.sendRequest(
{
url:'/api/detail'
method: 'POST',
query,
validator: createDetailValitor
},
);
}
}
export const detailService = new DetailServices();
/*
enum Status {
SUCCESS,
FAIL
}
export interface ListItem {
name: string;
age: number;
status: Status;
children?: ListItem[];
}
export interface DetailResponse extends BaseResponse {
data: {
id: string;
name: string;
tags: string[];
list: ListItem[];
};
}
*/
const listItem = safeObjectWrap(
object({
status: safeNativeEnum(Status).default(Status.FAIL),
name: safeString().default(''),
age: safeNumber().default(20)
}).passthrough()
);
const createDetailValidator = () => {
type Input = input<typeof listItem> & {
children?: Input[];
};
type Output = output<typeof listItem> & {
children?: Output[];
};
const schema: ZodType<Output, ZodTypeDef, Input> = listItem
.extend({
children: lazy(() => schema.array())
})
.partial({ children: true });
return createBaseResponseValidator(
safeObjectWrap(
object({
id: safeString(),
name: safeString(),
tags: safeArray(safeString()).default([]),
list: safeArray(schema)
}).partial({ list: true })
)
);
};
export const detailSchema = createDetailValidator();
import { detailSchema } from './validator';
type DetailSchema = z.infer<typeof detailSchema>;
export type Data = DetailSchema['data'];
export type List = DetailSchema['data']['list'];
export type DataWithoutId = Omit<Data, 'id'>;