ts-best-practice
TypeScript:我都传了 type 了,能不能给我自动推导出 data 类型啊? - 知乎
确实是很有用的小体操,第一次了解这种用法就是发现了 dom 的 addEventListener 可以根据 name 自动推断 event 的类型,ctrl 点进去学习了,跟答主描述的如出一辙
巧用联合类型
Dinner
要么有 fish
要么有 bear
。
// Not good.
interface Dinner1 {
fish?: number,
bear?: number,
}
// Awesome!
type Dinner2 = {
fish: number,
} | {
bear: number,
}
一些区别:
let d1: Dinner1 = {} // Opps
d1 = {fish:1, bear:1} // Opps
let d2: Dinner2 = {} // Protected!
d2 = {fish:1, bear:1} // Protected!
if ('fish' in d2) {
// `d2` has `fish` and no `bear` here.
} else {
// `d2` has `bear` and no `fish` here.
}
巧用查找类型 + 泛型 +keyof
interface API {
'/user': { name: string },
'/menu': { foods: Food[] },
}
const get = <URL extends keyof API>(url: URL): Promise<API[URL]> => {
return fetch(url).then(res => res.json())
}
上面的定义极大地增强了代码提示:
- 枚举类型不会有提示
- 是否必须是字符串? 属性名形式不带引号可以吗?
巧用显式泛型
$('button')
是个 DOM 元素选择器,可是返回值的类型是运行时才能确定的,除了返回 any
,还可以
function $<T extends HTMLElement>(id: string):T {
return document.getElementById(id)
}
// Tell me what element it is.
$<HTMLInputElement>('input').value
函数泛型不一定非得自动推导出类型,有时候显式指定类型就好。
巧用 as Const
const fetchOption = {
mode: 'same-origin',
credentials: 'include',
};
fetch('/api', fetchOption); // Error!
这因为 mode
的类型被推导为 string
而不是 'same-origin'
,credentials
同理。
推荐的做法是声明合理的类型:
const fetchOptions: RequestInit = {
mode: 'same-origin',
credentials: 'include',
};
如果要的类型很难取到,可以
const fetchOptions = {
mode: 'same-origin' as const,
credentials: 'include' as const,
};
// Or
const fetchOptions = {
mode: 'same-origin',
credentials: 'include',
} as const;
详见 const assertions 。
巧用 [number] 下标
通常,我们会定义一些枚举:
type Drink = 'Beer' | 'Wine' | 'Water';
以及一个全量的数组,和一个阻止酒吧爆炸的方法:
const DRINK_LIST: Drink[] = ['Beer', 'Wine', 'Water'];
const checkDrink = (drink: any): drink is Drink => {
return DRINK_LIST.includes(drink);
};
// 但这并不能保证 `DRINK_LIST` 是枚举值的全量列表:
const DRINK_LIST: Drink[] = ['Beer', 'Wine']; // Oh, I forgot water!
或许有某种技巧能定义出期望的全量列表元组。
但这里提供一种简便的写法 —— 先定义全量列表,再获取枚举类型:
const DRINK_LIST = ['Beer', 'Wine', 'Water'] as const;
type Drink = (typeof DRINK_LIST)[number]; // Equals to 'Beer' | 'Wine' | 'Water'.
详见 indexed-access-types 。
关于枚举值,我们也常用到映射,可以配合 上一篇 的【巧用 Record 类型】使用。
巧用 Omit + &
有时候,我们希望“继承”一个类型,并且“重写”其中一些属性:
type Base = {
foo: number;
bar: number;
};
// ❌ Interface 'A' incorrectly extends interface 'Base'.
interface A extends Base {
foo: string;
};
// ❌ B['foo'] is never.
type B = Base & {
foo: string;
};
可以先 Omit 掉:
interface C extends Omit<Base, 'foo'> {
foo: string;
};
type D = Omit<Base, 'foo'> & {
foo: string;
};
详见 Omit。
React
巧用类型查找 + 类方法
我们通常会在 React 组件中把方法传下去
class Parent extends React.PureComponent {
private updateHeader = (title: string, subTitle: string) => {
// Do it.
};
render() {
return <Child updateHeader={this.updateHeader}/>;
}
}
interface ChildProps {
updateHeader: (title: string, subTitle: string) => void;
}
class Child extends React.PureComponent<ChildProps> {
private onClick = () => {
this.props.updateHeader('Hello', 'Typescript');
};
render() {
return <button onClick={this.onClick}>Go</button>;
}
}
其实可以在 ChildProps
中直接引用类的方法
interface ChildProps {
updateHeader: Parent['updateHeader'];
}
两个好处:不用重复写方法签名,能从方法调用跳到方法定义 。
巧用 [a, b] as Const
React.useState()
返回 [state, setState]
的结构,方便调用方解构和命名:
const [title, setTitle] = React.useState();
这是一种很棒的设计,我们也效仿的话:
const makeGetSet = (initialValue: string) => {
let value = initialValue;
const setValue = (v: string) => value = v;
const getValue = () => value;
return [getValue, setValue];
};
const [getName, setName] = makeGetSet('14');
const currentName = getName(); // Error! But why?
原因是 [0, '']
会被推导为类型 (number | string)[]
。加 as const
可推断为元组:
const toGetSet = (initialValue: string) => {
let value = initialValue;
const setValue = (v: string) => value = v;
const getValue = () => value;
return [getValue, setValue] as const;
};
const [getName, setName] = toGetSet('14');
const currentName = getName(); // Great!
业务中,自定义 hook 比较多用:
const useFlag = (initialValue = false) => {
const [flag, setFlag] = React.useState(initialValue);
const up = React.useCallback(() => setFlag(true), []);
const down = React.useCallback(() => setFlag(false), []);
return [flag, up, down] as const;
};
const [modalVisible, showModal, hideModal] = useFlag();
增强语法
除了实现 ECMAScript 标准之外,TypeScript 团队也推进了诸多语法提案,比如可选链操作符(?.
)11、空值合并操作符(??
)12、Throw 表达式 13、正则匹配索引 14 等。
这些语法,不光是在 ts 中可以使用,甚至有一些在 js 中也可以使用
! 非空断言
this.element = document.getElementById('foof')!;
加一个感叹号,表示不可能为空,让代码提示忽略去
初始化一个空对象
可以类型注释初始化一个空数组,但是却不能初始化一个空对象
这是因为 ListFormatWithProject 限制的是元素,formatList 本身只需要是一个数组就可以了
const formatList: ListFormatWithProject[] = [];
type test = ListFormatWithProject[];
const arr:test = []; 此时则会报错
const projectMap: ProjectMap = {} as ProjectMap;
类型依赖
class 的一个属性依赖另一个属性的类型:通过泛型和接口属性访问实现
问题在于如何实现多层的