TS 的核心能力在于给 JS 提供静态类型检查,是有类型定义的 JS 的超集,包括 ES5、ES5+ 和其他一些诸如泛型、类型定义、命名空间等特征的集合。
本次仅会针对类型声明部分配合示例进行着重介绍,更详细的内容以及特性可以查看 Typescript handbook以及changelog。
typescript 的子类型是基于 结构子类型 的,只要结构可以兼容,就是子类型(Duck Type)
class Test {
x: number;
}
function get(test: Test) {
return test.x;
}
class Test2 {
x: number;
}
const test2 = new Test2();
// Passed
get(test2);
java、c++ 等传统静态类型语言是基于 名义子类型 的,必须显示声明子类型关系(继承),才可以兼容
子类型中必须包含源类型所有的属性和方法
function get(test: { x: number }) {
return test.x;
}
const test = {
x: 1,
y: "2",
};
// Passed
get(test);
注意: 如果直接传入一个对象字面量是会报错
function get(test: { x: number }) {
return test.x;
}
// Error!
// Argument of type '{ x: number; y: string; }' is not assignable to parameter of type '{ x: number; }'.
// Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
get({ x: 1, y: "2" });
这是 ts 中的另一个特性,叫做: excess property check,当传入的参数是一个对象字面量时,会进行额外属性检查
对于函数类型来说,函数参数的类型兼容是反向的,我们称之为 逆变 返回值的类型兼容是正向的,称之为 协变
我们允许一个函数类型中,返回值类型是协变的,而参数类型是逆变的。返回值类型是协变的,意思是 A ≼ B 就意味着 (T → A) ≼ (T → B) 。参数类型是逆变的,意思是 A ≼ B 就意味着 (B → T) ≼ (A → T) ( A 和 B 的位置颠倒过来了)
允许不变的列表(immutable)在它的参数类型上是协变的,但是对于可变的列表(mutable),其参数类型则必须是不变的(invariant),既不是协变也不是逆变
逆变与协变并不是 TS 中独有的概念,在其他静态语言(java 中数组即是逆变又是协变)中也有相关理念
若
如果我们现在有三个类型 Animal、 Human、 Man,那么肯定存在下面的关系:
Man ≼ Human ≼ Animal
所以存在 Animal => Man 是 Human => Human 的子类型,可以简单理解为对于参数的类型由于逆变可以接收参数类型的父类型,对于函数的返回值由于协变可以接收返回值类型的子类型
函数的参数为多个时可以转化为 Tuple 的类型兼容性,长度大的是长度小的子类型,再由于函数参数的逆变特性,所以函数参数少的可以赋值给参数多的(参数从前往后需一一对应)
具体 demo 和示例可以参考链接
What are covariance and contravariance?
在 TypeScript 中, 参数类型是双向协变的 ,也就是说既是协变又是逆变的,而这并不安全。但是现在你可以在 TypeScript 2.6 版本中通过 --strictFunctionTypes 或 --strict 标记来修复这个问题。
逆变协变本质上还是为了满足里式替换
任何类型都能分配给 unknown,但 unknown 不能分配给其他基本类型,而 any 可以分配和被分配任意类型
function wrapper(callback: () => void) {}
function test(): number {
return 1;
}
// works well
wrapper(test)
function throwErr(): never {
throw new Error("an error");
}
const age = 18;
throwErr();
// Unreachable code detected.
age.toFixed(2);
never 还可以用于联合类型的幺元:
// string | number
type T = string | number | never;
通过在联合类型中 never 类型幺元的特性可以做到很多过滤的操作 如过滤 Human 中 age 和 name 之外的成员
type Human = {
age: number;
name: string;
lover: string;
gender: 1 | 0;
};
type Filter = {
[P in {
[K in keyof T]: K extends "age" | "name" ? K : never;
}[keyof T]]: T[P];
};
// type T = {
// age: number;
// name: string;
// }
type T = Filter;
// 上面只是为了试验 never 的幺元特性 Filter可以更简单的写法
// type Filter = {
// [K in Extract
一般可以使用 typeof 来进行类型保护来避免访问错误的属性或者方法
function test(b: any) {
if (typeof b === "string") {
// Property 'test' does not exist on type 'string'
b.test();
}
}
此外就是主要用来进行类型推断
const test = (a: string) => a.length;
const obj = {
a: 1,
b: false,
};
// (a: string) => number
type A = typeof test; // (x: string) => number
// {
// a: number;
// b: boolean;
// }
type B = typeof obj;
获取接口的所有键,返回键的联合类型
type Test = {
a;
b;
};
// 'a' | 'b'
type Res = keyof Test;
对联合类型进行遍历
type Test = {
a;
b;
};
// type Res = {
// a: string;
// b: string;
// }
type Res = {
[K in keyof Test]: string;
};
需要注意的是只可以在 type 声明的类型下使用,如interface声明下会抛出A mapped type may not declare properties or methods.
A computed property name in an interface must refer to an expression whose type is a literal type or a ‘unique symbol’ type. The left-hand side of an arithmetic operation must be of type ‘any’, ‘number’, ‘bigint’ or an enum type. The right-hand side of an ‘in’ expression must not be a primitive.
索引访问,类似于 js 的元语法
type Test1 = {
a;
b;
};
type Test2 = [1, 2];
// any
type Res1 = Test["a"];
// 2
type Res2 = Test2[1];
这类语法可以逃脱掉类型检查的限制如调用一个没有明确声明的全局变量属性window[’undefinedVar’]
而不会获得报错,但相应的在使用的时候也不会获得任何类型提示
interface Foo {
bar: number;
bas: string;
}
const foo = {} as Foo;
然而,如下例子中的代码将会报错,尽管使用者已经使用了类型断言:
function test(a: number): void {}
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.(2352)
// Bad
// test('' as number);
// Good
test('' as unknown as number);
TypeScript 是怎么确定单个断言是否足够?
上面提到过ts的逆变和协变,当 S
类型是 T
类型的子集,或者 T
类型是 S
类型的子集时,S
能被成功断言成 T
。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以借助 any
或者unknown
进行双重断言。
接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现
tsconfig.json 是 ts 的编译器(tsc)将 ts 编译为 js 的配置文件,在开发和编译阶段提供支持(语法检查,代码依赖等)
使用 tsconfig.json
如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录。 tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:
当命令行上指定了输入文件时,tsconfig.json 文件会被忽略
参数:
以 .d.ts 结尾的文件用来给 ts 提供类型定义的文件 如果一个文件有扩展名 .d.ts,这意味着每个根级别的声明都必须以 declare 关键字作为前缀。这有利于让开发者清楚的知道,在这里 TypeScript 将不会把它编译成任何代码,同时开发者需要确保这些在编译时存在。
如何使用?
扩展原生对象
默认的一些原生对象上是不存在一些自定义挂载的属性,所以不可以直接赋值,可以输用方括号赋值语句,但是读操作时也必须用 [] ,并且没有类型提示。
可以通过类型合并
declare interface Window {}
// or
declare global {
interface Window {}
}
扩展第三方库
import Vue from "vue";
declare module "vue/types/vue" {
interface Vue {}
}
处理其他扩展名文件
declare module "*.css" {
const content: any;
export default content;
}
如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型
interface Person {
age: number
}
interface Person {
name: string
}
// Good
const man: Person = { age: 100, name: 'foo' }
commonjs
对象
const $ = require('jquery')
declare module '$' {
export function forEach(callback: () => any): void
}
declare module 'koa' {
function random_name(): any
export=random_name
}
export declare var age: number
//orType
declare var name: string
export { name }
// or
export default name
TS 的模块分为全局模块和文件模块,默认情况下,我们所写的代码是位于全局模块下的
// a.ts
const age = 18;
如果在另一个文件中使用 age,ts 的检查是正常的(全局)
// b.ts
console.log(age);
将当前模块变为局部的文件模块只需要当前文件存在任意的 export 或者 import 语句即可
TypeScript 团队似乎并不喜欢第三种模式,因此请尽可能避免使用第三种模式
共有两种可用的模块解析策略:Node 和 Classic。 你可以使用 --moduleResolution 标记来指定使用哪种模块解析策略。若未指定,那么在使用了 --module AMD | System | ES2015 时的默认值为 Classic,其它情况时则为 Node。
有一个对 module 的非相对导入 import { b } from "module"
,它是在/root/src/folder/A.ts 文件里,会以如下的方式来定位"module":
Node 的模块解析通过分别查找/root/src、/root、/三种路径下的 node_modules
[/root/src/|/root/|/]node_modules/module.ts
[/root/src/|/root/|/]node_modules/module.tsx
[/root/src/|/root/|/]node_modules/module.d.ts
[/root/src/|/root/|/]node_modules/module/package.json (如果指定了"types"属性)
[/root/src/|/root/|/]node_modules/module/index.ts
[/root/src/|/root/|/]node_modules/module/index.tsx
[/root/src/|/root/|/]node_modules/module/index.d.ts
Classic 的寻址方式 这种策略在以前是 TypeScript 默认的解析策略。 现在,它存在的理由主要是为了向后兼容。
/root/src/folder/module.ts
/root/src/folder/module.d.ts
/root/src/module.ts
/root/src/module.d.ts
/root/module.ts
/root/module.d.ts
/module.ts
/module.d.ts
相关链接:
泛指的类型,关键目的是在成员之间(类以及类成员或者函数的入参、返回值等)提供有意义的约束
interface Animal {
type: T
}
const Jessica: Animal<'people'> = {
type: 'people'
}
const Wangcai: Animal<'dog'> = {
type: 'dog'
}
具体泛型的使用配合下面大量的示例
TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test:
T extends U ? X : Y
The type above means when T is assignable to U the type is X, otherwise the type is Y.
interface Human {
name: string
age: number
secondLanguage: string
lover?: Human
}
const Jessica: Human = {
name: 'jessica',
age: 21,
secondLanguage: 'Frence'
}
但不是每个人都有第二语言
const Lucia: Human = { // Type error secondLanguage is required
name: 'jessica',
age: 21,
}
但是如果要重新写一个又显的冗余重复
两种方案
思路是获取去除的这个属性的 key,首先有
type TEST = {
[P in K]: T[P]
}
这个泛型 K 其实就是泛型 T 的键值,即 P extends keyof T
type TEST = {
[P in K]: T[P]
}
接下来需要定义一个函数来移除泛型 K 中指定的那个属性 key
type EXCLUDE = T extends U ? never : T;
最后定义支持去除指定 key 的函数 OMIT
type OMIT = {
[P in EXCLUDE]: T[P]
}
再利用这个函数来声明新的 Human 类型
type Human_exclude = OMIT
const Lucia: Human = { // pass
name: 'jessica',
age: 21,
}
但是死板的去除指定属性某些场景可能不满足需求(后面可能会动态的想 TS 对象添加某个 k)
如果我们将指定的属性(language)定义为可选,那么同样满足需求
方法通用上面的移除:
type type OPTIONAL_K = {
[P in K]?: T[P];
} &
OMIT;
const Lucia: Human = { // pass
name: 'jessica',
age: 21,
}
Lucia.luanguage = 'en' // pass
infer
表示在 extends 条件语句中待推断的类型变量
infer
type ParamType = T extends (param: infer P) => any ? P : T;
infer P 表示待推断的函数参数
如果 T 能赋值给 (param: infer P) => any
,则结果是 (param: infer P) => any
类型中的参数的类型 P,否则返回为 T
type PICK = {
[P in K]: T[P];
};
type A = PICK;
type PARTIAL = {
[P in keyof T]?: T[P];
};
type F = PARTIAL;
type REQUIRED = {
[P in keyof T]-?: T[P];
};
type G = REQUIRED;
type REQUIRED_K = {
[P in K]-?: T[P];
} &
OMIT;
type E = REQUIRED_K;
type SIG = {
key: {
key: {
key: any
}
}
}
type READONLY_RECURSIVE = {
readonly [P in keyof T]: T[P] extends {[index: string]: any} ? READONLY_RECURSIVE : T[P];
};
const test: READONLY_RECURSIVE = {
key: {
key: {
key: 123
}
}
}
test.key.key = 123 // err: Cannot assign to 'key' because it is a read-only property.ts(2540)
type RETURN any> = T extends () => infer R ? R : never;
type CONSTRUCTOR_P any> = T extends new (
...args: infer P
) => any
? P
: any[];
function bark() {
return "string";
}
type Eat = () => number;
type BarkReturn = RETURN; // string
type EatReturn = RETURN; // number
class HumanBeing {
constructor(name: string, age: number) {}
}
type HB = CONSTRUCTOR_P; // [string, number]
type PROMISE_LIKE = {
then(
resolve?: (value?: T) => T1 | PROMISE_LIKE | undefined | null,
reject?: (value?: any) => T2 | PROMISE_LIKE | undefined | null
): PROMISE_LIKE;
};
type PROMISE = {
then(
resolve?: (value?: T) => T1 | PROMISE_LIKE | undefined | null,
reject?: (value?: any) => T2 | PROMISE_LIKE | undefined | null
): PROMISE_LIKE;
catch(value?: any): T1 | PROMISE_LIKE | undefined | null;
};
当你安装 TypeScript
时,会顺带安装一个 lib.d.ts
声明文件。这个文件包含 JavaScript 运行时以及 DOM 中存在各种常见的环境声明。
可以通过指定 --noLib
的编译器命令行标志(或者在 tsconfig.json
中指定选项 noLib: true
)从上下文中排除此文件。
interface PromiseLike
interface Promise
type Partial
type Required
type Readonly
type Pick
type Record
type Exclude
type Extract
type Omit
type NonNullable
type Parameters any>
type ConstructorParameters any>
type ReturnType any>
type InstanceType any>
此外还内置 es5 其他函数和方法以及数据类型等的声明如果 mac vsc 有安装 typescript 拓展可以借助 vsc 打开,具体路径在/Applications/Visual Studio
Code.app/Contents/Resources/app/extensions/node_modules/typescript/lib/lib.es5.d.ts
https://github.com/LeetCode-OpenSource/hire/blob/master/typescript_zh.md
假设有类型
interface Action {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}));
}
setMessage(action: Action) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}
经过 Connect 函数之后,返回值类型为
type Result {
delay(input: T): Action;
setMessage(action: T): Action;
}
从表面来看我们要做的有三点:
step1:构造转换 1 和 2 的函数也是最关键的一步
type Transform = {
[K in keyof T]: T[K] extends ((input: Promise) => Promise<{
payload: infer U;type:string
}>)
? ((input: P) => Action)
: T[K] extends ((action: Action) => {
payload: infer U;
type:string
})
?((action: P) => Action)
: never;
}
type Temp = Transform
// type Temp = {
// count: never;
// message: never;
// delay: (input: number) => Action;
// setMessage: (action: Date) => Action;
// }
step2:构造工具函数将 step1 中得到的 never 类型的无关类型去除
type OmitNever = {
[K in keyof T]: T[K] extends Function ? never: K
}[keyof T];
type temp = OmitNever
// type temp = "count" | "message"
step3: 借助 Omit 移除 step1 中 step2 的 key
type Connect = Omit, OmitNever>;
type Result = Connect
// type Result = {
// delay: (input: number) => Action;
// setMessage: (action: Date) => Action;
// }
type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((
k: infer I,
) => void)
? I
: never
((k: A) => void) | ((k: B) => void)
而不是 (k: A|B) => void
为什么 ((k: A) => void) | ((k: B) => void)
的参数是 A & B?
因为函数参数是逆变的,我们假设有一个变量能同时传给 (k: A) => void 和 (k: B) => void
,那么这个变量的类型应该是 A & B 而不是 A | B
P extends K 意味着所有 K 都可以无条件被 P 替换
一个函数能被 (k: A) => void 和 (k: B) => void
无条件替换,那么那个函数接受的参数必然既是 A 又是 B
((k: A) => void) | ((k: B) => void)
*不是应该被分开处理吗?如果分开处理那得到的结果依然会是 A | B,那么又是为什么能够得出 A & B 呢?conditional-type
extends 左边联合类型被拆开判断的情况只会出现在左边是一个类型参数的情况:大概就是 type F = T extends any 的这个左边是会被拆开,而 type F = A | B extends any 的左边就不会被拆开。
type Union = {age} | {number}
type Union2 = number | string
type Intersection = UnionToIntersection // {age} & {number}
type Intersection2 = UnionToIntersection // never
主体的思路是递归的将联合类型的每一项取出来放入元组同时移除这一项,最后将递归结束后的元组返回;同时也要写一些基本的辅助函数:
step1: 将 Union 类型转为由函数类型组成的交叉类型
// union to intersection of functions
// 42 =====> (42) => void =====> ((a: U, ...r: T) infer R
type UnionToIoF =
(U extends any ? (k: (x: U) => void) => void : never) extends
((k: infer I) => void) ? I : never
step2: 借助特性每次获取最后一个元素
type UnionPop = UnionToIoF extends (a: infer A) => void ? A : never;
step3: 将获取的类型推入元组
type Prepend =
((a: U, ...r: T) => void) extends (...r: infer R) => void ? R : never;
step4: 移除推入的类型
// 借助内置类型 Exclude
type Exclude = T extends U ? never : T;
step5: 最后一步借助上面的工具函数写转换的递归
type UnionToTupleRecursively = {
0: Result;
1: UnionToTupleRecursively>, Prepend, Result>>
}[[Union] extends [never] ? 0 : 1];
Q: 此处为何要借助 {}[] 的形式来作为递归的终止条件而不是直接使用三目呢?
type aliases are not like interfaces. interfaces are named types, where as type aliases are just aliases. internally as well they are treated differently, the compiler aggressively flatten types aliases to their declarations.
type alias 不允许调用自身 这里使用索引方式 {}[]
Ts 4.1.3 版本后取消此限制
// wrong
type UnionToTupleRecursively = [Union] extends [never] ? Result : UnionToTupleRecursively>, Prepend, Result>>
Below is the complete code.
type UnionToIoF =
(U extends any ? (k: (x: U) => void) => void : never) extends
((k: infer I) => void) ? I : never
// return last element from Union
type UnionPop = UnionToIoF extends (a: infer A) => void ? A : never;
// prepend an element to a tuple.
type Prepend<U, T extends any[]> =
((a: U, ...r: T) => void) extends (...r: infer R) => void ? R : never;
type UnionToTupleRecursively<Union, Result extends any[]> = {
0: Result;
1: UnionToTupleRecursively<Exclude<Union, UnionPop<Union>>, Prepend<UnionPop<Union>, Result>>
}[[Union] extends [never] ? 0 : 1];
type UnionToTuple = UnionToTupleRecursively<U, []>;
// support union size of 43 at most
type Union43 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43;
type A_ = UnionPop<Union43>
// type A_ = 43
type B_ = UnionToIoF<Union43>
// type B_ = ((x: 1) => void) & ((x: 2) => void) & ((x: 3) => void) & ((x: 4) => void) & ((x: 5) => void) & ((x: 6) => void) & ((x: 7) => void) & ((x: 8) => void) & ((x: 9) => void) & ((x: 10) => void) & ((x: 11) => void) & ((x: 12) => void) & ((x: 13) => void) & ((x: 14) => void) & ((x: 15) => void) & ((x: 16) => void) & ... 26 more ... & ((x: 43) => void)
type UnionTest = string | number | 'sss' | {age:123}
type TupleTest = UnionToTuple<UnionTest>;
// type TupleTest = [a: string, a: number, a: {
// age: 123;
// }]
type Tuple = UnionToTuple<Union43>;
// type Tuple = [a: 1, a: 2, a: 3, a: 4, a: 5, a: 6, a: 7, a: 8, a: 9, a: 10, a: 11, a: 12, a: 13, a: 14, a: 15, a: 16, a: 17, a: 18, a: 19, a: 20, a: 21, a: 22, a: 23, a: 24, a: 25, a: 26, a: 27, a: 28, a: 29, a: 30, a: 31, a: 32, a: 33, a: 34, a: 35, a: 36, a: 37, a: 38, a: 39, a: 40, a: 41, a: 42, a: 43]
一个有意思的是批量处理联合类型时支持的最大长度为 43(没确定是否和版本有关),否则会抛出 Type instantiation is excessively deep and possibly infinite.ts(2589) 的异常
Ts 4.1.3 版本后取消此限制
除却本次分享之外 TS 还有很多其他相关的很多特性包括
以及更多高阶运用
scanner.ts
)parser.ts
)binder.ts
)checker.ts
)做一个合格的Typescript工程师,感受体操的魅力!
从type-challenges开始:
https://github.com/type-challenges/type-challenges