写了一段时间ts,在从头学习一遍,温故而之新
ts的一些技巧
1、巧用注释
通过/** */形式的注释可以给 TS 类型做标记,编辑器会有更好的提示:
/** A cool guy. */
interface Person {
/** A cool name. */
name: string,
}
2、巧用注释 进阶
注释有很多规范的字段,基本和 JSDOC 一致。但不用着急翻文档,在 /** */ 里输入 @ 就可以看到丰富的选择
3、巧用 typeof
我们一般先写类型,再使用:
interface Opt {
timeout: number
}
const defaultOption: Opt = {
timeout: 500
}
有时候可以反过来:
const defaultOption = {
timeout: 500
}
type Opt = typeof defaultOption
当一个 interface 总有一个字面量初始值时,可以考虑这种写法以减少重复代码。
4、巧用联合类型
// ? Not good.
interface Dinner1 {
fish?: number,
bear?: number,
}
// ? Awesome!
type Dinner2 = {
fish: number,
} | {
bear: number,
}
5、巧用查找类型+泛型+keyo
interface API {
'/user': { name: string },
'/menu': { foods: Food[] },
}
const get = (url: URL): Promise => {
return fetch(url).then(res => res.json())
}
6、巧用显式泛型
$('button') 是个 DOM 元素选择器,可是返回值的类型是运行时才能确定的,除了返回 any ,还可以
function $(id: string):T {
return document.getElementById(id)
}
// Tell me what element it is.
$('input').value
函数泛型不一定非得自动推导出类型,有时候显式指定类型就好。
7、巧用 DeepReadonly
type DeepReadonly = {
readonly [P in keyof T]: DeepReadonly;
}
const a = { foo: { bar: 22 } }
const b = a as DeepReadonly
b.foo.bar = 33 // Hey, stop!
8、巧用 Omit
import { Button, ButtonProps } from './components/button'
type Omit = Pick>
type BigButtonProps = Omit
function BigButton(props: BigButtonProps) {
return Button({ ...props, size: 'big' })
}
一些知识点
1、交叉类型 (intersection types)与联合类型 (union types)
TypeScript 1.6 引入了交叉类型作为联合类型 (union types) 逻辑上的补充. 联合类型 A | B 表示一个类型为 A 或 B 的实体, 而交叉类型 A & B 表示一个类型同时为 A 或 B 的实体.
function extend(first: T, second: U): T & U {
let result = {};
for (let id in first) {
result[id] = first[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
result[id] = second[id];
}
}
return result;
}
var x = extend({ a: "hello" }, { b: 42 });
var s = x.a;
var n = x.b;
type LinkedList = T & { next: LinkedList };
interface Person {
name: string;
}
var people: LinkedList;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
interface A { a: string }
interface B { b: string }
interface C { c: string }
var abc: A & B & C;
abc.a = "hello";
abc.b = "hello";
abc.c = "hello";
2、abstract (抽象的) 类和方法
TypeScript 1.6 为类和它们的方法增加了 abstract 关键字. 一个抽象类允许没有被实现的方法, 并且不能被构造.
abstract class Base {
abstract getThing(): string;
getOtherThing() { return 'hello'; }
}
let x = new Base(); // 错误, 'Base' 是抽象的
// 错误, 必须也为抽象类, 或者实现 'getThing' 方法
class Derived1 extends Base { }
class Derived2 extends Base {
getThing() { return 'hello'; }
foo() {
super.getThing();// 错误: 不能调用 'super' 的抽象方法
}
}
var x = new Derived2(); // 正确
var y: Base = new Derived2(); // 同样正确
y.getThing(); // 正确
y.getOtherThing(); // 正确
3、keyof和查找类型
在JavaScript中属性名称作为参数的API是相当普遍的,但是到目前为止还没有表达在那些API中出现的类型关系。
输入索引类型查询或keyof,索引类型查询keyof T产生的类型是T的属性名称。keyof T的类型被认为是string的子类型。
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string
4、索引访问类型,也称为查找类型
type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string
将这种模式和类型系统的其它部分一起使用,以获取类型安全的查找
function getProperty(obj: T, key: K) {
return obj[key]; // 推断类型是T[K]
}
function setProperty(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
let x = { foo: 10, bar: "hello!" };
let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string
let oops = getProperty(x, "wargarbl"); // 错误!"wargarbl"不存在"foo" | "bar"中
setProperty(x, "foo", "string"); // 错误!, 类型是number而非string
5、映射类型
使用现有类型并使其每个属性完全可选
interface Person {
name: string;
age: number;
location: string;
}
Person的可选属性类型将是这样:
interface PartialPerson {
name?: string;
age?: number;
location?: string;
}
使用映射类型,PartialPerson可以写成是Person类型的广义变换:
type Partial = {
[P in keyof T]?: T[P];
};
type PartialPerson = Partial;
映射类型可以表示许多有用的类型转换:
// 保持类型相同,但每个属性是只读的。
type Readonly = {
readonly [P in keyof T]: T[P];
};
// 相同的属性名称,但使值是一个Promise,而不是一个具体的值
type Deferred = {
[P in keyof T]: Promise;
};
// 为T的属性添加代理
type Proxify = {
[P in keyof T]: { get(): T[P]; set(v: T[P]): void }
};
6、Partial,Readonly,Record和Pick
Partial和Readonly,如前所述,是非常有用的结构。你可以使用它们来描述像一些常见的JS程序:
function assign(obj: T, props: Partial): void;
function freeze(obj: T): Readonly;
因此,它们现在默认包含在标准库中。
我们还包括两个其他实用程序类型:Record和Pick。
// 从T中选取一组属性K
declare function pick(obj: T, ...keys: K[]): Pick;
const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
// 对于类型T的每个属性K,将其转换为U
function mapObject(obj: Record, f: (x: T) => U): Record
const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
其他不错的地址
weekly 2.0-2.9 精读
react的技巧
TypeScript 2.8下的终极React组件模式
react-redux-typescript-guide
复杂 React 应用中的TypeScript 3.0实践
库react-redux-typescript-guide
其他一些知识点2.8
Deep Readonly
interface DeepReadonlyArray extends ReadonlyArray> {}
type DeepReadonlyObject = {
readonly [P in keyof T]: DeepReadonly
}
type DeepReadonly =
T extends (infer R)[]
? DeepReadonlyArray
: T extends Function
? T
: T extends object
? DeepReadonlyObject
: T
Mutable
type Mutable = { -readonly [P in keyof T]: T[P] }
利用 ReturnType 直接拿 type 可以减少 boilerplate
还有两个很常见但一直没有被收进的
Diff
type Diff = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]
2.8 内置了 Exclude 作为官方版的 Diff
Omit
type Omit = Pick>
TS 中的内置类型简述
Partial 局部的;偏爱的;不公平的 美 ['pɑrʃəl]
ts 中的实现
// node_modules/typescript/lib/lib.es5.d.ts
type Partial = {
[P in keyof T]?: T[P];
};
这个类型的用处就是可以将某个类型里的属性加上 ? 这个 modifier ,加了这个 modifier 之后那些属性就可以为 undefined 了。
举个例子,我有个接口 Person ,里面定义了两个必须的属性 name 和 age。
interface Person {
name: string;
age: number;
}
// error , property age is missing.
const axes: Person = {
name: 'axes'
}
如果使用了 Partial
type NewPerson = Partial;
// correct, because age can be undefined.
const axes: NewPerson = {
name: 'axes'
}
这个 NewPerson 就等同于
interface Person {
name?: string;
age?: number;
}
但是 Partial 有个局限性,就是只支持处理第一层的属性,如果我的接口定义是这样的
interface Person {
name: string;
age: number;
child: {
name: string;
age: number;
}
}
type NewPerson = Partial;
// error, property age in child is missing
const axes: NewPerson = {
name: 'axes';
child: {
name: 'whx'
}
}
可以看到,第二层以后的就不会处理了,如果要处理多层,就可以自己通过 Conditional Types 实现一个更强力的 Partial
export type PowerPartial = {
// 如果是 object,则递归类型
[U in keyof T]?: T[U] extends object
? PowerPartial
: T[U]
};
Required 必需的;(美)必修的 美 [rɪ'kwaɪrd]
// node_modules/typescript/lib/lib.es5.d.ts
type Required = {
[P in keyof T]-?: T[P];
};
这个类型刚好跟 Partial 相反,Partial 是将所有属性改成不必须,Required 则是将所有类型改成必须。
其中 -? 是代表移除 ? 这个 modifier 的标识。再拓展一下,除了可以应用于 ? 这个 modifiers ,还有应用在 readonly ,比如 Readonly 这个类型
// node_modules/typescript/lib/lib.es5.d.ts
type Readonly = {
readonly [P in keyof T]: T[P];
};
就可以给子属性添加 readonly 的标识,如果将上面的 readonly 改成 -readonly 就是移除子属性的 readonly 标识。
Pick 挑选;采摘;挖 美 [pɪk]
// node_modules/typescript/lib/lib.es5.d.ts
type Pick = {
[P in K]: T[P];
};
这个类型则可以将某个类型中的子属性挑出来,比如上面那个 Person 的类
type NewPerson = Pick; // { name: string; }
可以看到 NewPerson 中就只有个 name 的属性了,这个类型还有更有用的地方,等讲到 Exclude 类型会说明。
Record 记录,记载;标明;将...录音 美 [(for v.) rɪˈkɔrd; (for n.) ˈrekərd;ˈrɛkɚd]
// node_modules/typescript/lib/lib.es5.d.ts
type Record = {
[P in K]: T;
};
可以获得根据 K 中的所有可能值来设置 key 以及 value 的类型
type T11 = Record<'a' | 'b' | 'c', Person>; // { a: Person; b: Person; c: Person; }
Exclude 排除;排斥;拒绝接纳;逐出 美 [ɪk'sklʊd]
// node_modules/typescript/lib/lib.es5.d.ts
type Exclude = T extends U ? never : T;
个类型可以将 T 中的某些属于 U 的类型移除掉,举个例
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
可以看到 T 是 "a" | "b" | "c" | "d" ,然后 U 是 "a" | "c" | "f" ,返回的新类型就可以将 U 中的类型给移除掉,也就是 "b" | "d" 了。
那这个类型有什么用呢,在我看来,可以结合 Pick 类型使用。
在我们给 js 写声明的时候,经常会遇到我们需要 extend 某个接口,但是我们又需要在新接口中将某个属性给 overwrite 掉,但是这样经常会遇到类型兼容性问题。举个例子
interface Chicken {
name: string;
age: number;
egg: number;
}
我需要继承上面这个接口
// error, Types of property 'name' are incompatible
interface NewChicken extends Chicken {
name: number;
}
可以看到就会报错了,因为在 Chicken 中 name 是 string 类型,而 NewChicken 却想重载成 number 类型。很多时候可能有人就直接把 name 改成 any 就算了,但是不要忘了我们有个 Pick 的类型,可以把我们需要的类型挑出来,那就可以这样
// correct.
interface NewChicken extends Pick {
name: number;
}
可以看到,我们把 Person 中的类型做了挑选,只把 age 和 egg 类型挑出来 extend ,那么我复写 name 就没问题了。
不过再想一下,如果我要继承某个接口并且复写某一个属性,还得把他的所有属性都写出来么,当然不用,我们可以用 Exclude 就可以拿到除 name 之外的所有属性的 key 类型了。
type T01 = Exclude; // 'age' | 'egg'
然后把上面代码加到 extend 中就成了
// correct.
interface NewChicken extends Pick> {
name: number;
}
然后还可以把这个处理封装成一个单独的类型
type FilterPick = Pick>;
然后上面的 extend 的代码就可以写成这样,就更简洁了
interface NewChicken extends FilterPick {
name: number;
}
这样一来,我们就可以愉快的进行属性 overwrite 了。
ReturnType
// node_modules/typescript/lib/lib.es5.d.ts
type ReturnType any> = T extends (...args: any[]) => infer R ? R : any;
这个类型也非常好用,可以获取方法的返回类型,可能有些人看到这一长串就被绕晕了,但其实也是使用了 Conditional Types ,推论 ( infer ) 泛型 T 的返回类型 R 来拿到方法的返回类型
实际使用的话,就可以通过 ReturnType 拿到方法的返回类型,如下的示例
function TestFn() {
return '123123';
}
type T01 = ReturnType; // string
ThisType
// node_modules/typescript/lib/lib.es5.d.ts
interface ThisType { }
可以看到声明中只有一个接口,没有任何的实现,说明这个类型是在 ts 源码层面支持的,而不是通过类型变换,那这个类型有啥用呢,是用于指定上下文对象类型的。
interface Person {
name: string;
age: number;
}
const obj: ThisType = {
dosth() {
this.name // string
}
}
这样的话,就可以指定 obj 里的所有方法里的上下文对象改成 Person 这个类型了。跟
const obj = {
dosth(this: Person) {
this.name // string
}
}
差不多效果。
NonNullable
// node_modules/typescript/lib/lib.es5.d.ts
type NonNullable = T extends null | undefined ? never : T;
根据实现可以很简单的看出,这个类型可以用来过滤类型中的 null 及 undefined 类型
type T22 = '123' | '222' | null;
type T23 = NonNullable; // '123' | '222'
其他常用
PromiseType
export type PromiseType> = T extends Promise ? R : any;