当我们讨论 TypeScript 时,我们在讨论什么?
TypeScript 中的 Decorator 较为特殊,为 angular 团队和 TypeScript 团队交易的结果,有兴趣可自行搜索相关资料。而且近期 EcmaScript 规范中的 decorator 提案内容发生了剧烈变动,建议等此语法标准完全稳定后再在生产项目中使用。
本文只讨论图中蓝色部分。
JSDoc 也能标注类型,为什么要用 TypeScript?
JSDoc 只是注释,其标注没有约束作用
TS 有—checkJs 选项,但不好用
TS 会自动推断函数返回值类型,为什么要多此一举标注出来?
契约高于实现
检查返回值是否写错
写 return 时获得提醒
自行配置
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"moduleResolution": "node"
}
react 项目运行 create-react-app ${项目名} —scripts-version=react-scripts-ts
虽然在写法上,这两个操作符与位运算逻辑操作符相同。但在语义上,它们与位运算刚好相反。
位运算的表现:
1001 | 1010 = 1011 // 合并1
1001 & 1010 = 1000 // 只保留共有1
在 TypeScript 中的表现:
interface IA {
a: string
b: number
}
type TB = {
b: number
c: number[]
}
type TC = IA | TB; // TC类型的变量的键只需包含ab或bc即可,当然也可以abc都有
type TD = IA & TB; // TD类型的变量的键必需包含abc
对于这种表现,可以这样理解: & 表示必须同时满足多个契约, | 表示满足任意一个契约即可。
interface 和 type 两个关键字因为其功能比较接近,常常引起新手的疑问:应该在什么时候用 type,什么时候用 interface?
interface 的特点如下:
建议库的开发者所提供的公共 api 应该尽量用 interface/class,方便使用者自行扩展。举个例子,我之前在给腾讯云 Cloud Studio 在线编辑器开发插件时,因为查阅到的 monaco 文档是 0.15.5 版本(当时的最新版本)的,而 Cloud Studio 使用的 monaco 版本为 0.14.3,缺失了一些我需要的 API,所以需要手动 polyfill 一下。
/**
* Cloud Studio使用的monaco版本较老0.14.3,和官方文档相比缺失部分功能
* 另外vscode有一些特有的功能,必须适配
* 故在这里手动实现作为补充
*/
declare module monaco {
interface Position {
delta(deltaLineNumber?: number, deltaColumn?: number): Position
}
}
// monaco 0.15.5
monaco.Position.prototype.delta = function (this: monaco.Position, deltaLineNumber = 0, deltaColumn = 0) {
return new monaco.Position(this.lineNumber + deltaLineNumber, this.column + deltaColumn);
}
与 interface 相比,type 的特点如下:
type Tuple = [number, string];
const a: Tuple = [2, 'sir'];
type Size = 'small' | 'default' | 'big' | number;
const b: Size = 24;
基本上所有用 interface 表达的类型都有其等价的 type 表达。但我在实践的过程中,也发现了一种类型只能用 interface 表达,无法用 type 表达,那就是往函数上挂载属性。
interface FuncWithAttachment {
(param: string): boolean;
someProperty: number;
}
const testFunc: FuncWithAttachment = ...;
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3; // 有类型提醒
extends 本意为 “拓展”,也有人称其为 “继承”。在 TypeScript 中,extends 既可当作一个动词来扩展已有类型;也可当作一个形容词来对类型进行条件限定(例如用在泛型中)。在扩展已有类型时,不可以进行类型冲突的覆盖操作。例如,基类型中键 a 为 string,在扩展出的类型中无法将其改为 number。
type A = {
a: number
}
interface AB extends A {
b: string
}
// 与上一种等价
type TAB = A & {
b: string
}
在前文我们已经看到类型实际上可以进行一定的运算,要想写出的类型适用范围更广,不妨让它像函数一样可以接受参数。TS 的泛型便是起到这样的作用,你可以把它当作类型的参数。它和函数参数一样,可以有默认值。除此之外,还可以用 extends 对参数本身需要满足的条件进行限制。
在定义一个函数、type、interface、class 时,在名称后面加上<> 表示即接受类型参数。而在实际调用时,不一定需要手动传入类型参数,TS 往往能自行推断出来。在 TS 推断不准时,再手动传入参数来纠正。
// 定义
class React.Component<P = {}, S = {}, SS = any> { ... }
interface IShowConfig<P extends IShowProps> { ... }
// 调用
class Modal extends React.Component<IModalProps, IModalState> { ... }
除了与、或等基本逻辑,TS 的类型也支持条件运算,其语法与三目运算符相同,为 T extends U ? X : Y 。这里先举一个简单的例子。在后文中我们会看到很多复杂类型的实现都需要借助条件类型。
type IsEqualType<A, B> = A extends B ? (B extends A ? true : false) : false;
type NumberEqualsToString = IsEqualType<number, string>; // false
type NumberEqualsToNumber = IsEqualType<number, number>; // true
在实际应用开发时有一种场景,当前作用域下可以访问某个变量,但这个变量并不由开发者控制。例如通过 Script 标签直接引入的第三方库 CDN、一些宿主环境的 API 等。这个时候可以利用 TS 的环境声明功能,来告诉 TS 当前作用域可以访问这些变量,以获得类型提醒。
具体有两种方式,declare 和三斜线指令。
declare const IS_MOBILE = true; // 编译后此行消失
const wording = IS_MOBILE ? '移动端' : 'PC端';
用三斜线指令可以一次性引入整个类型声明文件。
/// <reference path="../typings/monaco.d.ts" />
const range = new monaco.Range(2, 3, 6, 7);
基本类型,也可以理解为原子类型。包括 number、boolean、string、null、undefined、function、array、字面量(true,false,1,2,‘a’)等。它们无法再细分。
TypeScript 的复合类型可以分为两类: set 和 map 。set 是指一个无序的、无重复元素的集合。而 map 则和 JS 中的对象一样,是一些没有重复键的键值对。
// set
type Size = 'small' | 'default' | 'big' | 'large';
// map
interface IA {
a: string
b: number
}
// map => set
type IAKeys = keyof IA; // 'a' | 'b'
type IAValues = IA[keyof IA]; // string | number
// set => map
type SizeMap = {
[k in Size]: number
}
// 等价于
type SizeMap2 = {
small: number
default: number
big: number
large: number
}
// 索引取值
type SubA = IA['a']; // string
// 属性修饰符
type Person = {
age: number
readonly name: string // 只读属性,初始化时必须赋值
nickname?: string // 可选属性,相当于 | undefined
}
在 TypeScript 中,有以下几种常见的映射类型。它们的共同点是只接受一个传入类型,生成的类型中 key 都来自于 keyof 传入的类型,value 都是传入类型的 value 的变种。
type Partial<T> = { [P in keyof T]?: T[P] } // 将一个map所有属性变为可选的
type Required<T> = { [P in keyof T]-?: T[P] } // 将一个map所有属性变为必选的
type Readonly<T> = { readonly [P in keyof T]: T[P] } // 将一个map所有属性变为只读的
type Mutable<T> = { -readonly [P in keyof T]: T[P] } // ts标准库未包含,将一个map所有属性变为可写的
此类变换,在 TS 中被称为同态变换。在进行同态变换时,TS 会先复制一遍传入参数的属性修饰符,再应用定义的变换。
interface Fruit {
readonly name: string
size: number
}
type PF = Partial<Fruit>; // PF.name既只读又可选,PF.size只可选
由 set 生成 map
type Record<K extends keyof any, T> = { [P in K]: T };
type Size = 'small' | 'default' | 'big';
/*
{
small: number
default: number
big: number
}
*/
type SizeMap = Record<Size, number>;
保留 map 的一部分
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
/*
{
default: number
big: number
}
*/
type BiggerSizeMap = Pick<SizeMap, 'default' | 'big'>;
删除 map 的一部分
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
/*
{
default: number
}
*/
type DefaultSizeMap = Omit<BiggerSizeMap, 'big'>;
保留 set 的一部分
type Extract<T, U> = T extends U ? T : never;
type Result = 1 | 2 | 3 | 'error' | 'success';
type StringResult = Extract<Result, string>; // 'error' | 'success
删除 set 的一部分
type Exclude<T, U> = T extends U ? never : T;
type NumericResult = Exclude<Result, string>; // 1 | 2 | 3
获取函数返回值的类型。但要注意不要滥用这个工具类型,应该尽量多手动标注函数返回值类型。理由开篇时提过, 契约高于实现 。用 ReturnType 是由实现反推契约,而实现往往容易变且容易出错,契约则相对稳定。另一方面,ReturnType 过多也会降低代码可读性。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function f() { return { a: 3, b: 2}; }
/*
{
a: number
b: number
}
*/
type FReturn = ReturnType<f>;
以上这些工具类型都已经包含在了 TS 标准库中,在应用中直接输入名字进行使用即可。另外,在这些工具类型的实现中,出现了 infer、never、typeof 等关键字,在后文我会详细解释它们的作用。
TS 原生的 Readonly 只会限制一层写入操作,我们可以利用递归来实现深层次的 Readonly。但要注意,TS 对最大递归层数做了限制,最多递归 5 层。
type DeepReadony<T> = {
readonly [P in keyof T]: DeepReadony<T[P]>
}
interface SomeObject {
a: {
b: {
c: number;
};
};
}
const obj: Readonly<SomeObject> = { a: { b: { c: 2 } } };
obj.a.b.c = 3; // TS不会报错
const obj2: DeepReadony<SomeObject> = { a: { b: { c: 2 } } };
obj2.a.b.c = 3; // Cannot assign to 'c' because it is a read-only property.
never 是 | 运算的幺元,即 x | never = x 。例如之前的 Exclude<Result, string> 运算过程如下:
infer 的作用是让 TypeScript 自己推断,并将推断的结果存储到一个临时名字中,并且只能用于 extends 语句中。它与泛型的区别在于,泛型是声明一个 “参数”,而 infer 是声明一个 “中间变量”。infer 我用得比较少,这里借用一下官方的示例。
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
typeof 用于获取一个 “常量” 的类型,这里的 “常量” 是指任何可以在编译期确定的东西,例如 const、function、class 等。它是从 实际运行代码 通向 类型系统 的单行道。理论上,任何运行时的符号名想要为类型系统所用,都要加上 typeof。但是 class 比较特殊不需要加,因为 ts 的 class 出现得比 js 早,现有的为兼容性解决方案。
在使用 class 时, class 名 表示实例类型, typeof class 表示 class 本身类型。没错,这个关键字和 js 的 typeof 关键字重名了 :)。
const config = { width: 2, height: 2 };
function getLength(str: string) { return str.length; }
type TConfig = typeof config; // { width: number, height: number }
type TGetLength = typeof getLength; // (str: string) => number
我在项目中遇到这样一种场景,需要获取一个类型中所有 value 为指定类型的 key。例如,已知某个 React 组件的 props 类型,我需要 “知道”(编程意义上)哪些参数是 function 类型。
interface SomeProps {
a: string
b: number
c: (e: MouseEvent) => void
d: (e: TouchEvent) => void
}
// 如何得到 'c' | 'd' ?
分析一下这里的思路,我们需要从一个 map 得到一个 set,而这个 set 是 map 的 key 的 子集,筛选子集的 条件 是 value 的类型。要构造 set 的子集,需要用到 never ;要实现条件判断,需要用到 extends ;而要实现 key 到 value 的访问,则需要索引取值。经过一些尝试后,解决方案如下。
type GetKeyByValueType<T, Condition> = {
[K in keyof T]: T[K] extends Condition ? K : never
} [keyof T];
type FunctionPropNames = GetKeyByValueType<SomeProps, Function>; // 'c' | 'd'
这里的运算过程如下:
// 开始
{
a: string
b: number
c: (e: MouseEvent) => void
d: (e: TouchEvent) => void
}
// 第一步,条件映射
{
a: never
b: never
c: 'c'
d: 'd'
}
// 第二步,索引取值
never | never | 'c' | 'd'
// never的性质
'c' | 'd'
TypeScript 只发生在编译期,因此我们可以在代码中加入一些符号,来给予编译器一些提示,使其按我们要求的方式运行。
类型转换的语法为 < 类型名> xxx 或 xxx as 类型名 。推荐始终用 as 语法,因为第一种语法无法在 tsx 文件使用,而且容易和泛型混淆。一般只有这几种场景需要使用类型转换:自动推断不准;TS 报错,想不出更好的类型编写方法,手动抄近路;临时 “放飞自我”。
在使用类型转换时,应该遵守几个原则:
在编写 TS 程序时,我们的目标是让类型覆盖率无限接近 100%。
! 的作用是断言某个变量不会是 null / undefined,告诉编译器停止报错。这里由用户确保断言的正确。它和刚刚进入 EcmaScript 语法提案 stage 3 的 Optional Chaining 特性不同。Optional Chaining 特性可以保证访问的安全性,即使在 undefined 上访问某个键也不会抛出异常。而 ! 只是消除编译器报错,不会对运行时行为造成任何影响。
// TypeScript
mightBeUndefined!.a = 2
// 编译为
mightBeUndefined.a = 2
用于忽略下一行的报错,尽量少用。
enum 在 TS 中出现的比较早,它引入了 JavaScript 没有的数据结构(编译成一个双向 map),入侵了运行时,与 TypeScript 宗旨不符。用 string literal union('small' | 'big' | 'large')可以做到相同的事,且在 debug 时可读性更好。如果很在意条件比较的性能,应该用二进制 flag 加位运算。
// TypeScript
enum Size {
small = 3,
big,
large
}
const a:Size = Size.large; // 5
// 编译为
var Size;
(function (Size) {
Size[Size["small"] = 3] = "small";
Size[Size["big"] = 4] = "big";
Size[Size["large"] = 5] = "large";
})(Size || (Size = {}));
const a = Size.large; // 5
我们应该编写有类型系统的 JavaScript,而不是能编译成 JavaScript 的 Java/C#。任何一个 TypeScript 程序,在手动删去类型部分,将后缀改成 .js 后,都应能够正常运行。
原文来自:http://www.alloyteam.com/2019/07/13796/
近些日子,我使用了新语言编程,从JavaScript,切确地说是Elm,转成TypeScript。在本文中,我将继续深挖一些我非常喜欢的TypeScript特性。
TypeScript 和 JavaScript 是目前项目开发中较为流行的两种脚本语言,我们已经熟知 TypeScript 是 JavaScript 的一个超集,但是 TypeScript 与 JavaScript 之间又有什么样的区别呢?
Nerv_是一款由京东凹凸实验室打造的类 React 前端框架,基于虚拟 DOM 技术的 JavaScript(TypeScript) 库。它基于React标准,提供了与 React 16 一致的使用方式与 API。
交叉类型:将多个类型合并为一个类型、联合类型:表示取值可以为多种类型中的一种、混合类型:一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性、类型断言:可以用来手动指定一个值的类型
在做比较大的,多人合作的项目的时候,TypeScript会更加地适合,这得益于它的可读性,面向对象性以及易于重构的特点。但如果只是自己做小程序,不需要太多人参与的时候,JavaScript则会更加简单。
有两种方式安装TypeScript,如何创建第一个TypeScript文件,在TypeScript中,可以使用interface来描述一个对象有firstName和lastName两个属性,TypeScript支持JavaScript的新功能,其中很重要的一个功能就是基于类的面向对象编程
使用TypeScript已经有了一段时间,这的确是一个好东西,虽说在使用的过程中也发现了一些bug,不过都是些小问题,所以整体体验还是很不错的。有关TypeScript声明类型声明相关的目前就总结了这些比较常用的
谷歌在很早之前就张开双臂拥抱 Web 应用程序,Gmail 已经发布 14 年了。当时,JavaScript 的世界是疯狂的。Gmail 工程师不得不为 IE 糟糕的垃圾回收算法捏一把汗,他们需要手动将字符串文字从 for 循环中提取出来,以避免 GC 停顿
TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。
差不多两年前,我在一个创业团队中开始了一个全新的项目。用到的全都是类似Microservices,docker,react,redux这些时髦的东西。我在前端技术方面积累了一些类似的经验
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!