TypeScript 你学废了吗
前言
TypeScript到目前为止,语言特性已经相当多相当复杂了。本文档着重于介绍那些特别有用、如果灵活运用会极大提高coding experience的语言特性。为了做到尽可能的覆盖大部分有用知识点,特性只会做简单的介绍,在实际的编码中可以慢慢积累这些类型的妙用。
Generic (范型)
范型形如下
function firstElement<Type>(arr: Type[]): Type {
return arr[0];
}
const s = firstElement(["a", "b", "c"]);
// s is of type 'string'
const n = firstElement([1, 2, 3]);
// n is of type 'number'
推断
function map<I, O>(arr: I[], func: (arg: I) => O): O[] {
return arr.map(func);
}
map函数根据传入的参数,动态的推断I,O的类型
类型约束
function strOrNum<T extends (number | string)>(input: T): T {
return input
}
// 传入的参数得是 string 或者是 number
范型类型
范型更广阔使用领域为范型类型
interface Box<T> {
content: T;
}
type Box<T> = T | Array<T>
Function Overloads (函数重载)
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678); // correct
const d2 = makeDate(5, 5, 5); // correct
const d3 = makeDate(1, 3); // error!
上面函数有三种重载签名,函数调用若均不符合其中任意一种函数签名,则会报错。
this的声明
this必须声明在函数参数声明中的第一个,且this在函数参数中的声明,不作为形参和实参。
class Handler {
info: string | undefined
handle(this: Handler, msg: string) {
[this](http://this.info/)[.info](http://this.info/) = msg
}
}
const h = new Handler()
h.handle('hello');
特殊类型
void
void 表示不返回值的函数的返回值。另外返回类型为 void 的上下文类型不会强制函数不返回某些内容。 另一种说法是具有 void 返回类型(类型 vf = () => void)的上下文函数类型,在实现时,可以返回任何其他值,但会被忽略。
function noop() {
return;
}
// function noop(): void
type voidFunc = () => void;
const func: voidFunc = () => {
return true;
};
const value = func();
// const value: void
// 即使func函数有返回值,也不影响,value的类型为void
这种行为存在,因此即使 Array.prototype.push 返回一个数字并且 Array.prototype.forEach 方法需要一个返回类型为 void 的函数,以下代码也是有效的。
const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));
unknown
unknown类似any,但是比any安全,因为通过unknown做任何操作都是不合法的。因此尽量用unknown,不用any。 这里结合typescirpt4.0中一个新的特性做个例子。
try {
// do something crazy here
} catch(e) {
console.error(e.message, 'error occur')
// 默认情况下,这里的e是any类型
}
try {
// do something crazy here
} catch(e: unknown) {
console.error(e?.message, 'error occur')
// typescript 4.0 之后 catch 这里的e支持标示类型。因此更安全的做法是使用unknown来定义e 不然any就相当于裸奔。很容易出现type error
}
never
never 类型表示从未观察到的值。 在返回类型中,这意味着函数抛出异常或终止程序的执行。
function fail(msg: string): never {
throw new Error(msg)
}
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
Declaration Merging (声明合并)
Interface合并
interface Box {
height: number;
width: number;
}
interface Box {
weight: number;
}
let box: Box = {height: 4, with: 5, weight: 6}
同名的interface会被合并
type 以及 interface的 区别:
interface
可以被extends
, 但是type
不行,interface
是一个对象(而且目前支持的应该只有[]
和{}
), 但是type
可以玩出花来的。。因为interface
本质上是需要支持{} | []
的一些约束的
Namespaces合并
和interface一样,namespaces也会合并。namespace中可以包含类型以及值。
namespace Animals {
export class Zebra {}
}
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Dog {}
}
// 等同于
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Zebra {}
export class Dog {}
}
如果遇到重复命名的类型或者值并且均导出的,在合并中会报错。(即使是相同命名的namespaces,没有导出的值或者类型在另外一个中是无法访问的)
Assertion & type checker (断言相关) & type narrowing
type narrowing
首先来了解一下 type narrowing Type narrowing的方式很多,主要有如下方式
-
typeof
type guard -
Truthiness narrowing
-
Equality narrowing
-
The
in
operator narrowing -
instanceof
narrowing
typeof 以及 instanceof
使用typeof以及instanceof关键词判断的方式也可以收敛类型。
function strOrNum(input: string | number) {
if (typeof input === 'string') {
// input is string now
} else {
// input is number
}
}
Truthiness 和 equality
这两种方式也很类似,因此合并来讲。
declare const obj: Record<string, unknown> | null
declare const bool1: boolean | undefined
declare const bool2: boolean | null
if (obj) {
// obj: Record<string, unknown>
} else {
// obj: null
}
if (obj) {
// obj: Record<string, unknown>
} else {
// obj: null
}
if (bool1 === bool2) {
// bool1: boolean
// bool2: boolean
}
in
操作符
in操作符对于判断对象是否包含某一属性或者方法是很有效的,typescript中可以利用in操作符达到type-narrrowing的效果
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
as
有好几种断言方式。最简单一种是使用 as 关键词。
declare const str: any
let res = (str as string).toUpperCase()
declare const num: number
let res2 = (num as unknown as string).toLowerCase()
// num类型不能直接收敛的情况下,需要先将num断言为更宽泛的类型 例如unknown
Type Guards 类型守卫
可以通过自己实现类型守卫函数来将类型收敛性。类型守卫比typeof更加强大了。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
// ---cut---
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
// pet类型被收敛到了类型 Fish 上
pet.swim();
} else {
pet.fly();
}
断言函数 assert functions
如果发生意外,有一组特定的函数会抛出错误。 它们被称为“断言”函数。 例如,Node.js 有一个专门的函数,称为assert。
function multiply(x: any, y: any) {
assert(typeof x === "number");
assert(typeof y === "number");
// both x and y is number now
return x * y;
}
Typescript中可以自定义实现assert函数。有两种形式的assert函数定义方式
第一种
function myAssert(param: any, msg?: string): asserts param {
if (! param) {
throw new Error(msg);
}
}
function multiply(x: any, y: any) {
myAssert(typeof x === "number");
myAssert(typeof y === "number");
// both x and y is number now
return x * y;
}
第二种
declare function isOdd(param: unknown): asserts param is 1 | 3 | 5;
function num(input: number) {
isOdd(input)
console.log(input)
// input is 1 | 3 | 5
}
! 后缀
!后缀可以从类型中排除掉null和undefined类型
declare const obj: {name: string} | null
const name = obj!.name // obj被断言为了 {name: string}
考虑到type安全,尽量不要用这个标识符。
第三方库实现的type assertion
GitHub - unional/type-plus: Additional types and types adjusted utilities for TypeScript
Mapped Types
in 标识符
映射类型是一种泛型类型,它使用 PropertyKeys 的联合(通常通过 keyof 创建)来迭代键以创建类型。 in标识符会遍历Type,返回key
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
+ - 标识符
- 表示增加属性,- 表示消去属性。+ 符号可以不写,但是写上提高可读性。具体例子如下
type Student = {
id: string;
readonly name?: string;
};
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// {
// id: string;
// name?: string;
// }
type CreateImmutable<Type> = {
+readonly [Property in keyof Type]: Type[Property];
};
// {
// readonly id: string;
// readonly name?: string;
// }
type CreateOptional<Type> = {
[Property in keyof Type]+?: Type[Property];
};
// {
// id?: string;
// readonly name?: string;
// }
type CreateRequired<Type> = {
[Property in keyof Type]-?: Type[Property];
};
// {
// id: string;
// readonly name: string;
// }
as
TypeScript4.1 新增符号,配合下文中将要提到的Template Literal Types(模板文字类型)功能强大。
type MappedType<Type> = {
[Properties in keyof Type as (string | number)]: Type[Properties]
}
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
Conditional Type
如下形式为conditional type。可以做很多骚操作的关键。
SomeType extends OtherType ? TrueType : FalseType;
Recursive Conditional Types
typescript4.1 引入。有了recursive conditional types,各种循环迭代就可以支持了。有了这个特性支持可以写很多高级类型。
type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;
如果递归深度超过50层会报错。这点还是特别nice的。
Template Literal Types
同样是 typescript4.1 引入。
type World = "world";
type Greeting = `hello ${World}`;
// "hello world"
type EnumNum = 1 | 2 | 3
type EnumGroup = `${EnumNum}${EnumNum}`
// "11" | "12" | "13" | "21" | "22" | "23" | "31" | "32" | "33"
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
// ^ string
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
// ^ number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
有了template literal types 可以实现很多之前实现不了的type 下面这例子是实现camelCase转换
type ToCamel<S extends string> =
S extends `${infer Head}-${infer Tail}`
? `${Lowercase<Head>}${Capitalize<ToCamel<Tail>>}`
: S extends Uppercase<S>
? Lowercase<S>
: S extends `${Capitalize<infer Head>}${Capitalize<infer Tail>}`
? S
: void
type ToCamelCase<S extends string | number | bigint> = Uncapitalize<ToCamel<S & string>>
type T1 = ToCamelCase<"camel-case"> // camelCase
type T2 = ToCamelCase<"camelCase"> // camelCase
type T3 = ToCamelCase<"CamelCase"> // camelCase
type T4 = ToCamelCase<"CAMELCASE"> // camelcase
Variadic Tuple Types (可变元组类型)
Typescript4.0新增特性。
type Foo<T extends unknown[]> = [string, ...T, number];
type T1 = Foo<[boolean]>; // [string, boolean, number]
type T2 = Foo<[number, number]>; // [string, number, number, number]
type T3 = Foo<[]>; // [string, number]
export type FirstElem<T extends readonly unknown[]> = T[0];
export type LastElem<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : undefined;
export type RemoveFirst<T extends readonly unknown[]> = T extends readonly [unknown, ...infer U] ? U : [...T];
// [unknown, ...infer U] 可以替换为 [any, ...infer U]
export type RomoveLast<T extends readonly unknown[]> = T extends readonly [...infer U, unknown] ? U : [...T];
// [...infer U, unknown] 可以替换为 [...infer U, any]
type TS1 = FirstElem <[number, string, boolean]>; // number
type TS2 = LastElem <[number, string, boolean]>; // boolean
type TS3 = RemoveFirst <[number, string, boolean]>; // [string, boolean]
type TS4 = RomoveLast <[number, string, boolean]>; // [number, string]
来看一个实际例子。有一个函数merge,实现的功能是merge输入的object。可以输入2个至任意多个object。来看看这种type是如何实现的。
type Merge<F extends Record<string | number, unknown>, S extends Record<string | number, unknown>> = {
[P in keyof F | keyof S]: P extends keyof Omit<F, keyof S> ? F[P] : P extends keyof S ? S[P] : never;
};
type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
? {}
: T['length'] extends 1
? T[0]
: T extends [infer P, infer Q, ...infer U]
? P extends Record<string | number, unknown>
? Q extends Record<string | number, unknown>
? U extends Record<string | number, unknown>[]
? MergeArr<[Merge<P, Q>, ...U]>
: MergeArr<[Merge<P, Q>]>
: never
: never
: never;
declare function mixin<
T extends Record<string | number, unknown>,
P extends [Record<string | number, unknown>, ...Record<string | number, unknown>[]]
>(target: T, ...rest: P): MergeArr<[T, ...P]>
const a = mixin({name: 'joe'}, {sex: 'male'}, {haveFun: [1,2,3]})
// {name: string; sex: string; haveFun: number[]}
/*
** 这里写得这么麻烦全是因为typescript不够智能 **
按理来说 下面的代码已经足够了,但typescirpt报错
type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
? {}
: T['length'] extends 1
? T[0]
: T extends [infer P, infer Q, ...infer U]
? U extends Record<string | number, unknown>[]
? MergeArr<[Merge<P, Q>, ...U]>
: MergeArr<[Merge<P, Q>]>
: never;
** 另外 如果说typescript支持下面这种写法 也应该可以避免相当多冗余代码 但是typescript依然不支持
type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
? {}
: T['length'] extends 1
? T[0]
: T extends [infer P, infer Q, ...infer U]
? U extends Record<string | number, unknown>[]
? [P, Q] extends [Record<string | number, unknown>, Record<string | number, unknown>]
? MergeArr<[Merge<P, Q>, ...U]>
: never
: MergeArr<[Merge<P, Q>]>
: never;
*/
Infer
上文中出现了infer标识符,infer标识符含义是根据输入的值去推断infer所在位置的type的类型。 这个概念刚开始可能比较抽象,看多了代码就会理解了。具体说明转 官网
// tuple中使用
type InferSecondElem<T extends unknown[]> = T extends [unknown, infer P, ...unknown[]] ? P : void
type A = InferSecondElem<[number, boolean, unknown, string]> // boolean
// template中使用
type Split<S extends string, D extends string> =
string extends S ? string[] :
S extends '' ? [] :
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
[S];
type B = Split<'split,by,comma', ','> // ["split", "by", "comma"]
// function type中使用
type RetType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type Res = RetType<() => number> // number
Type utils
官方提供了部分常用的utility types。通常大部分都在typescript包下的lib.es5.d.ts文件中。另外社区也有不少的封装。例如 这里 。可以实现加减乘除等各种骚操作。