背景介绍
JavaScript创立20多年,已经从当初只是为网页添加琐碎交互的小型脚本语言发展成应用最广泛的跨平台语言之一。
虽然用JavaScript可编写的程序无论从规模、范围、复杂性都呈指数级增长,但因为JavaScript弱类型特性使得其在项目管理层面表现的相当差劲。科技巨头微软为了解决这样的尴尬问题推出了Typescript。
内容梗概
之所以命名为Typescript文档是因为本文是对日常开发中的ts语法全链路指引。
看完收获
1、熟悉常用的ts语法
2、清楚常用的编译选项
3、正确预判各种场景下的ts特性
Typescript特性
Static type-checking(静态类型检测)
先看一个js的例子
const message = "message";
message.toFixed(2);
根据以上js代码段,如果在浏览器中执行,你会得到如下错误:
我们再将上一段放入ts文件中
在代码运行之前,ts就会给我们一条错误消息,这样的特性就叫static type-checking(静态类型检测)。
Non-exception Failures(非异常的失败)
还是以一个js实例引入话题:
const user = {
name: "Daniel",
age: 26,
};
user.location; // returns undefined
如我们熟知的那样,在js中取一个对象不存在的属性,那么它会返回undefined。
我们将代码移入ts文件:
ts会通过静态检测机制报错。像这种在js中不属于异常范畴的ts报错,就称之Non-exception Failures(非异常的失败)。
此类失败(静态类型检测不通过),我们再列出几个
未正确的调用的方法:
逻辑错误:
Types for Tooling(语法提示)
前面我们讲了ts的异常拦截还有非异常拦截,足以让我们感受到ts的错误拦截的强悍之处,但是ts还有个重磅功能:前置的语法提示!
看图:
当我们在支持ts语法提示的编辑器(本文使用vscode演示)键入字符串变量之后的"."操作,会自动提示出来很多的相关操作。
tsc, TypeScript 编译器
我们一直在谈论类型检查,但我们还没有使用我们的类型检查器。 下面隆重推出tsc,TypeScript 编译器。
我们通过npm install -g typescript来安装。
tsc hello.ts
tsc hello.ts可以将hello.ts编译成js文件
Erased Types(擦除的类型)
什么是擦除的类型,当我们使用tsc对hello.ts文件进行编译时,我们能得到如下结果
转换前:
转换后:
Downleveling(语法降级)
细心的你会发现,tsc指令除了会将代码中的类型擦除之外,还对代码进行了语法降级,模版字符的变成了字符拼接,是因为tsc默认采用ECMAScript 3或者ECMAScript 5标准来编译。
如果我们想要指定编译规范,可以这么写:tsc --target es2015 hello.ts
ts的基础类型:string,number, boolean
ts中使用最频繁的类型就是字符、数字和布尔值。有一点要注意,这几个类型表述都是小写,千万不要跟 js当中的String, Number, Boolean混淆。
Arrays(数组类型)
常见的数组类型表达T[]和Array
[1,2,3,4]一个数字集合的数组类型,我们可以用number[]及Array
记住一点[number]
可不是数组类型,它是一个标准的 Tuples(元组)。元组具体是啥,后续有详细介绍。
any(任意类型)
如其名,如果设置了某个变量的类型为any类型,那么不单单是这个变量本身类型不会被检测,它的属性和方法也会绕过ts的类型检测,都成立。
Type Annotations on Variables(在变量声明的同时指定其类型)
先看怎么定义:
let myName: string = "Alice";
定义方式就是”变量:类型“这样的方式
但是ts有个特性:类型推断。如上例子咱们也可以直接:
let myName = "Alice";
ts也会根据赋值类型,推断出来变量的类型。
关于标量是否需要指定其类型,官方说:在定义变量的时候,尽可能的少指定其类型:
For the most part you don’t need to explicitly learn the rules of inference. If you’re starting out, try using fewer type annotations than you think - you might be surprised how few you need for TypeScript to fully understand what’s going on.
Functions(函数)
一个例子:
function greet(name: string): string {
return 'Hello, ' + name.toUpperCase() + '!!'
}
函数的参数类型和返回类型如上例所示。后续有详尽的函数篇幅介绍。
Object Types(对象类型)
一个例子:
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
类型表述 { x: number; y: number },将所有的对象属性一一列出就是对象类型的基础用法。
参数可选:
function printCoord(pt: { x: number; y?: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
咱们先快速过基础用法,后续会有详细的对象类型语法介绍。
Union Types(联合类型)
一个例子:
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
语法表达:number | string,联合类型的意思恰如其名,用来表达一个变量包含一种以上的类型表达。
联合类型带来的麻烦:
如何解决:
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
我们通过一定的条件过滤将类型做了划分,最终解决了类型交叉的问题。
Type Aliases( 类型别名语法:Type)
前面我说过联合类型的用法,那么是不是每次用到联合类型都要字面量的方式“type1 | type2”去定义呢?
当然不是,Type关键的字申明类型别名可以帮你解决:
type ID = number | string;
Interfaces( 类型语法:Interfaces)
一个例子:
interface Point {
x: number;
y: number;
}
Differences Between Type Aliases and Interfaces(Type和Interface的区别)
以上是官方的说法,作为一个TS重度支持者,我用一句大白话说下,我对与type和interface的用法区别,type用作支撑类型“计算”,比如我们在原类型基础之上重新定义一个新类型。
看个例子:
interface Person {
name: string
age?: number
}
type NewPerson = Person & {
age: number
}
Type Assertions(类型断言)
何为断言,示例中找的第一个例子是获取dom元素的例子,正如我们清楚的那样,获取 dom要么是获取到null要么是获取到具体的dom元素。那么如果我们后续想要使用这个dom元素的操作,就需要使用具体的元素类型,比如:HTMLCanvasElement。对于这样的场景,我们可以在语法中加入断言(我非常确定我获取到的dom元素非null且就是我认为的那种类型!)。断言是 TS中的肯定语气。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = document.getElementById("main_canvas");
const a = (expr as any) as T; //有时候需要绕一下
要点1 :as是最常用的断言方式,但是如果看到
要点2:咱们实际开发过程中会碰到有些类型没办法直接断言的情况,那么我们就需要断言两次,通常会使用as any或者as unknown作为第一层断言“(expr as any) as T”
Literal Types( 字面类型)
先看一个基础例子:
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'.
再来一个联合类型的例子:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
再来一个联合类型的例子:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
Non-null Assertion Operator (Postfix!)(ts的非undefined和null的断言修饰符“!”)
TypeScript还有一种特殊语法,用于在不进行任何显式检查的情况下从类型中删除null和undefined。具体实例如下:
function liveDangerously(x?: number | null) {
console.log(x.toFixed());
//Object is possibly 'null' or 'undefined'
console.log(x!.toFixed());
}
ts中所有的具体值都能作为类型使用,这样的语法就叫做字面类型
Enums(枚举)
枚举是TypeScript为数不多的特性之一,它不是JavaScript的类型级扩展。
Numeric enums(数值枚举)
enum Direction {
Up = 1,
Down,
Left,
Right,
}
enum Direction {
Up,
Down,
Left,
Right,
}
数值枚举,默认的起始数值是1,后续以此递增。如果指定了头部值,那么默认值就会变成指定值。
String enums(字符枚举)
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
枚举常用在字面化表达一个变量的不同类型值,比如:性别。
Less Common Primitives(不常用的类型)
bigint
从ES2020开始,JavaScript中有一个用于非常大整数的原语BigInt:
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
symbol
JavaScript中有一个原语,用于通过函数Symbol()创建全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
Narrowing(类型过筛)
这张图是我找了半天用来表示TS语法的Narrowing的图,我给语法想了一个中文特性的词:过筛(网络上都习惯说窄化,但是我觉得太生硬)。正如图片所示,Narrowing其实就是表述TS类型归类的语法集。
typeof type guards(typeof拦截)
JavaScript 支持 typeof 运算符,它可以返回变量的类型字符。 TypeScript也同样,看结果列表:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
利用typeof缩小类型范围
function printAll(str: string | number): void {
if (typeof str === 'string') {
console.log(str)
} else {
console.log(str + '')
}
}
Truthiness narrowing(真实性)
先看个例子:
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
上例拦截了数字0,如果没有在线人数则显示"无人在此",在js中某些值做if判断,会是false。
这些值如下:
0
NaN
"" (the empty string)
0n (the bigint version of zero)
null
undefined
Equality narrowing(等于判断)
来个例子:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x);
console.log(y);
}
}
如上代码,如果想要x全等于y,那么只能是string类型。
再来个例子:
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) { //这是重点要考
console.log(container.value);
(property) Container.value: number
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
Thein operator narrowing(in操作符)
看个例子:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
再看个例子:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal;
// (parameter) animal: Fish | Human
} else {
animal;
// (parameter) animal: Bird | Human
}
}
当一个属性为可选字段的时候,用in判断,它会出现在true和false的两边。
instanceof narrowing(instance操作符)
一个例子:
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
(parameter) x: Date
} else {
console.log(x.toUpperCase());
(parameter) x: string
}
}
Assignments(赋值)
一个例子:
let x = Math.random() < 0.5 ? 10 : "hello world!";
x = 1;
console.log(x);
let x: number
x = true;
Type 'boolean' is not assignable to type 'string | number'.
console.log(x);
Using type predicates(使用类型谓词)
看个例子:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
The never type(never类型)
缩小范围时,你可以将类型缩小到一无所有的程度。 在这些情况下,TypeScript 将使用 never 类型来表示不应该存在的状态。
Exhaustiveness checking(详尽检测)
never 类型可分配给每种类型; 但是,没有任何类型可以分配给 never(除了 never 本身)。 这意味着你可以使用缩小范围并依靠从不出现在 switch 语句中进行详尽的检查。
看个例子:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
再看个例子:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
正如前面叙述的内容,never不能类型不能赋值成其他类型之外的任意类型。
More on Functions(关于function的更多内容)
函数是任何应用程序的基本构建块,无论它们是本地函数、从另一个模块导入的函数还是类上的方法。它们也是值,就像其他值一样,TypeScript有很多方法来描述如何调用函数。让我们学习如何编写描述函数的类型。
Function Type Expressions(函数表达式)
最简单的函数类型表达就是箭头函数
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
转变一下:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
Call Signatures(调用签名)
前面我们说了箭头函数的函数类型表达,接下来我们看下另外一种:
type DescribableFunction = {
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(" returned " + fn(6));
}
在JavaScript中,函数除了可以调用外,还可以具有属性。但是,函数类型表达式语法不允许声明属性。如果我们想用属性描述可调用的内容,可以在对象类型中编写调用签名:
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
Construct Signatures(构造签名)
还可以使用新操作符调用JavaScript函数。TypeScript将它们称为构造函数,因为它们通常创建一个新对象。您可以通过在调用签名前添加新关键字来编写构造签名:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
有些对象,如JavaScript的Date对象,可以使用new调用,也可以不使用new调用。您可以任意组合同一类型的调用和构造签名:
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
Generic Functions(范型函数)
通常编写一个函数,其中输入的类型与输出的类型相关,或者两个输入的类型以某种方式相关。让我们考虑一下返回数组第一个元素的函数:
function firstElement(arr: Type[]): Type | undefined {
return arr[0];
}
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);
Inference(主动推断)
注意,我们不必在这个示例中指定类型。类型由TypeScript推断(自动选择)。
我们也可以使用多个类型参数。例如,map的独立版本如下所示:
function map(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
以上两个例子,解释了范型函数提供了类型入口,让我们方便做类型管理。咱们再看一个最通用的范型例子:
interface ApiBaseRes {
code: number
msg: string
data: T
}
Constraints(范型的类型约束)
范型的类型也是可以有约束的,我们来看一个例子:
function longest(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
Working with Constrained Values(返回值约束)
前面我们说了范型可以约束参数,那么同样的也能约束返回值,一个例子:
function minimumLength(
obj: Type,
): Type {
return obj
}
再来看一个插常见的例子:
function minimumLength(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
Type '{ length: number; }' is not assignable to type 'Type'.
'{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
Specifying Type Arguments(指定类型参数)
例子说话:
function combine(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
const arr = combine([1, 2, 3], ["hello"]);
Type 'string' is not assignable to type 'number'.
修正方案:
const arr = combine([1, 2, 3], ["hello"]);
Push Type Parameters Down(推翻参数约束)
看个例子:
function firstElement1(arr: Type[]) {
return arr[0];
}
function firstElement2(arr: Type) {
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
记住一个规则:如果可能,请使用类型参数本身,而不是对其进行约束
Use Fewer Type Parameters(少使用范型的参数)
看两个表达式:
function filter1(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
function filter2 boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
规则:始终尽可能少的使用范型参数
Type Parameters Should Appear Twice(使用范型参数需要参数出现两次以上)
//错误的例子
function greet(s: Str) {
console.log("Hello, " + s);
}
greet("world");
//正确的例子
function greet(s: string) {
console.log("Hello, " + s);
}
规则:如果类型参数只出现在一个位置,请重新考虑是否确实需要它(想象一下一个一个只使用一次的变量)
Optional Parameters(可选参数)
两个例子说明:
//case 1
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
//case 2
function f(x = 10) {
// ...
}
Function Overloads(方法重载)
一些JavaScript函数可以在各种参数计数和类型中调用。例如,您可以编写一个函数来生成一个日期,该日期采用时间戳(一个参数)或月/日/年规范(三个参数)。
在TypeScript中,我们可以指定一个函数,该函数可以通过编写重载签名以不同的方式调用。为此,请编写一些函数签名(通常是两个或更多),然后是函数体:
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);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
Overload Signatures and the Implementation Signature(签名和实现签名)
看个例子:
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
为什么错呢?因为ts以申明签名为准。
另外实现签名还必须与重载签名兼容。例如,这些函数有错误,因为实现签名与重载不匹配:
先看个例子:
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
修复方案:
function fn(x: boolean): void;
function fn(x: string): void;
function fn(x: boolean|string) {}
Writing Good Overloads(编写良好的重载)
看个例子:
//两种尴尬
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) { //问题1:如果不写any那么就要写string | any[]
return x.length;
}
len(""); // OK
len([0]); // OK
//问题2:并不像我们想象的那样,调用也有问题
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call.
Overload 1 of 2, '(s: string): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'.
Type 'number[]' is not assignable to type 'string'.
Overload 2 of 2, '(arr: any[]): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'.
Type 'string' is not assignable to type 'any[]'.
来个最朴素的烹饪方式:
function len(x: any[] | string) {
return x.length;
}
规则:如果可能,请始终首选具有联合类型的参数,而不是重载参数。
Other Types to Know About(跟函数相关的其他类型)
void表示无返回
function noop():void {
}
记住void跟undefined不一样
object
在ts中object类型表示除了string、number、bingint、boolean、symbol、null、undefined类型之外其他类型。有人会问那么object是void吗?void只表示函数不返回。
unknown
unknown可以代表任意类型,但是不同于any,它不能像any那样拥有任意属性和方法。
never
never跟void类似,只用在函数的返回表达,看两个例子:
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'!
}
}
Function
一个例子:
function doSomething(f: Function) {
return f(1, 2, 3);
}
Rest Parameters and Arguments(剩余的函数参数和传参)
Rest Parameters(剩余的函数参数)
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
Rest Arguments(剩余的传参)
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
const args = [8, 5];
const angle = Math.atan2(...args);
Parameter Destructuring(参数解构)
一个例子:
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
Object Types(对象类型)
一个最常见的对象类型表达:
interface Person {
name: string;
age: number;
}
Property Modifiers(属性特性)
Optional Properties(可选属性)
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
(property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
(property) PaintOptions.yPos?: number | undefined
// ...
}
如何给可选属性添加默认值:
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
(parameter) xPos: number
console.log("y coordinate at", yPos);
(parameter) yPos: number
// ...
}
readonly Properties(只读属性)
一个例子:
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";
Cannot assign to 'prop' because it is a read-only property.
}
有人问,我能不能改变属性变成非只读,答案是能。在mapping modifiers里有解答
Index Signatures(索引签名?)
先看个例子:
interface StringArray {
[index: number]: string;
}
Extending Types(对象继承)
一个例子:
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
Intersection Types (交叉类型)
一个例子:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });
Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Generic Object Types(对象类型的范型)
一个例子:
interface Box {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box = { contents: "hello" };
boxA.contents;
(property) Box.contents: string
let boxB: StringBox = { contents: "world" };
boxB.contents;
(property) StringBox.contents: string
The Array Type(数组类型)
看个例子:
function doSomething(value: Array) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
数组表达Type[]或者Array
The ReadonlyArray Type(只读的数组类型)
一个例子:
function doStuff(values: ReadonlyArray) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.
}
Tuple Types(元组类型)
type StringNumberPair = [string, number];
function doSomething(pair: [string, number]) {
const a = pair[0];
const a: string
const b = pair[1];
const b: number
// ...
}
doSomething(["hello", 42]);
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];
Tuple type '[string, number]' of length '2' has no element at index '2'.
}
元组不仅是约束了元素类型,甚至还约束了元素个数。
readonly Tuple Types(只读的元组类型)
看个例子:
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
Cannot assign to '0' because it is a read-only property.
}
Type Manipulation (类型操作)
Creating Types from Types(以类型创建新类型)
Generics(范型)
范型的启动式,我想实现一个识别函数。
步骤一,实现识别固定类型:
function identity(arg: number): number {
return arg;
}
步骤二,放开类型限制
function identity(arg: any): any {
return arg;
}
步骤三、牵出范型
function identity(arg: Type): Type {
return arg;
}
验证调用:
let output = identity("myString");
第二种调用
let output = identity("myString");
ts会根据传入的参数类型,反推返回类型
Working with Generic Type Variables(使用范型的变量)
function identity(arg: Type): Type {
return arg;
}
Generic Classes(范型类)
class GenericNumber {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
Generic Constraints(范型约束)
范型作为一个预置类型参数,有没有一种语法可以约束其类型呢?答案是有的。
先看个例子:
function loggingIdentity(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.
return arg;
}
怎么破:
interface Lengthwise {
length: number;
}
function loggingIdentity(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
Using Type Parameters in Generic Constraints(利用范型约束入参)
可以声明受其他类型参数约束的类型参数。例如,这里我们想从给定名称的对象中获取属性。我们希望确保不会意外获取obj上不存在的属性,因此我们将在这两种类型之间放置一个约束:
function getProperty(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
Using Class Types in Generics(在范型中使用calss)
一个例子:
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Keyof Type Operator(keyof类型操作符)
一个例子:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
//type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//type M = string | number
请注意,在本例中,M的类型为string或者number-这是因为JavaScript对象键总是强制为字符串,所以obj[0]总是与obj[“0”]相同。
typeof and ReturnType type operator(typeof和ReturnType类型操作符)
先看typeof:
再看ReturnType:
typeof跟js的略微有所不同,它是一种返回类型的高级语法,如上图,它是可以返回类似这样的类型表达的:
() => { x:number, y:number }
Indexed Access Types(索引值方案类型)
一张图案例:
Conditional Types(条件类型)
基础的条件类型实例:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
type Example2 = string
//表达式
//SomeType extends OtherType ? TrueType : FalseType;
再来一个实例说明为什么要有条件类型这种语法:
//重载
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
//condition type
type NameOrId = T extends number
? IdLabel
: NameLabel;
function createLabel(idOrName: T): NameOrId {
throw "unimplemented";
}
let a = createLabel("typescript");
let a: NameLabel
let b = createLabel(2.8);
let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
let c: NameLabel | IdLabel
将原本需要分开表达的参数和返回值做了一定关联约束
Conditional Type Constraints(条件类型的约束)
一个例子:
type MessageOf = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf;
type EmailMessageContents = string
type DogMessageContents = MessageOf;
type DogMessageContents = never
再看一个例子:
type Flatten = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten;
type Str = string
// Leaves the type alone.
type Num = Flatten;
type Num = number
Inferring Within Conditional Types(条件类型的推断【infer】语法)
先来个帅气的开场:
type Flatten = Type extends Array ? Item : Type;
infer语法有点像一个别名申明,给类型定义一个别名,后续就能继续使用。
再来看一个函数返回使用infer语法:
type GetReturnType = Type extends (...args: any[]) => infer Return
? Return
: any;
type Num = GetReturnType<() => number>;
// type Num = number
type Str = GetReturnType<(x: string) => string>;
// type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
Distributive Conditional Types(条件类型的分配行为)
一个例子:
type ToArray = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray;
type StrArrOrNumArr = string[] | number[]
当我们使用范型和条件类型相结合,返回类型就变得可分配了。
Mapped Types(映射类型)
先看个例子:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
再来个变化的例子:
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags;
Mapping Modifiers(映射变换)
改变只读为非只读:
type CreateMutable = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable;
改变可选属性变成必填:
type Concrete = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete;
Key Remapping via as(通过as语法改变映射字段)
看一个公式:
type MappedTypeWithNewProperties = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
再来看一个实际的例子:
type Getters = {
[Property in keyof Type as `get${Capitalize}`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters;
我们来移除一个属性得到一个新类型:
type RemoveKindField = {
[Property in keyof Type as Exclude]: Type[Property]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField;
再看一个有趣的例子:
type ExtractPII = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII;
Template Literal Types(字面量的字符类型)
先看个例子:
type World = "world";
type Greeting = `hello ${World}`;
type Greeting = "hello world"
再看个组合类型的例子:
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
再来看个高级的例子:
type PropEventSource = {
on
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};
declare function makeWatchedObject(obj: Type): Type & PropEventSource;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
(parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
(parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
再来快速过几个类型变换:
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase
type ShoutyGreeting = "HELLO, WORLD"
type ASCIICacheKey = `ID-${Uppercase}`
type MainID = ASCIICacheKey<"my_app">
type Greeting = "Hello, world"
type QuietGreeting = Lowercase
type QuietGreeting = "hello, world"
type ASCIICacheKey = `id-${Lowercase}`
type MainID = ASCIICacheKey<"MY_APP">
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize;
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize;
Classes(类)
TS包含所有ES2015的class内容,并且在其基础上增加了类型申明,用来加强不同class和类型的关联描述。
Class Members(类的成员)
先看一个没有任何属性字段的最基础的class:
class Point {}
Fileds(字段)
class的公有(public)可写(writeable)属性:
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
如果我们想要给字段进行初始值设定:
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
class的类型推断:
const pt = new Point();
pt.x = "0";
//Type 'string' is not assignable to type 'number'.
Point类的x属性是数字类型,上例中我们设定其值为字符串的0,就被TS类型预警拦截了。
--strictPropertyInitialization(严格的属性初始化约束)
strictPropertyInitialization设置在TS编译选项中控制class字段必须要在构造方法(constructor)中完成初始化。 当我们设置了这样的选项之后,请看如下两个示例。
错误的示范:
class BadGreeter {
name: string;
}
//Property 'name' has no initializer and is not definitely assigned in the constructor.
正确的示例:
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
当然,如果我们有种方式,可以绕过TS的初始化检查,示例如下:
class OKGreeter {
// Not initialized, but no error
name!: string;
}
readonly(只读)
class的只读(readonly)是在定义属性时加前缀实现的,它会阻止属性在构造函数在外被改变。
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
//Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
//Cannot assign to 'name' because it is a read-only property.
Constructors(构造函数)
构造函数和普通函数基本一致,支持参数和重载
参数的例子:
class Point {
x: number;
y: number;
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
重载的例子:
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
构造函数跟普通函数之前的区别大致有两点:
1、构造函数整体不能被类型描述,构造函数的类型只能在class的申明表达式中描述。
2、构造函数不能有返回类型。
Super Calls(super语法调用)
如果我们在子类中想要使用父类的this.property系列属性和方法,我们需要在子类的构造函数中调用super,如果你不记得这个操作也不用怕,TS会提示你的。
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
//'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Methods(方法)
class中的function为了跟普通函数做区分,我们称之为方法,方法跟普通函数类似,一个例子:
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
只有一个注意点,在方法中使用class属性,必须用this.语法,否则会触发一些bug:
let x: number = 0;
class C {
x: string = "hello";
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
}
}
//Type 'string' is not assignable to type 'number'.
Getters / Setters
class拥有访问器:
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
三个注意点:
1、如果只有get那么这个属性就是只读
2、get属性的类型默认根据return推断得出
3、get和set成对属性,必须要时同一种成员特性描述(只读、可写等)
Index Signatures(索引签名)
class的属性也支持索引签名,直接看例子:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
Class Heritage(class的继承)
像其他面向对象语言一样,TS也可以继承基类
implements Clauses(implements语法)
你可以使用implements检查class是否按照interface的定义:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
Class 'Ball' incorrectly implements interface 'Pingable'.
pong() {
console.log("pong!");
}
//Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
}
class也可以实现多个interce:
class C implements A, B {}
extends Clauses(继承语法)
class可以继承自一个基类(base class),这个class可以包含基类的一切属性和方法,也可以在此基础上追加一些属性和方法。看个例子:
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
Overriding Methods(重写方法)
在子类中可以使用super关键词表示父类的实例。先看一个重写方法的例子:
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
上例中我们在子类中改写了父类的greet方法,设定了一个可选参数,如果我们将这个参数改成必填,那么就不符合父类的约束会报错:
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// Make this parameter required
greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
//Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
//Type '(name: string) => void' is not assignable to type '() => void'.
}
Type-only Field Declarations(只有类型的字段申明)
当我们TS编译标准设定值大于等于ES2022版本或者我们开启useDefineForClassFields
时,class的字段会在基础类构造函数执行完成之后初始化。当我们想要重写很多基础类的字段且只想要重新申明一个更准确的继承值时会出现一些问题。为了解决这样的问题,我们可以重新定义字段申明。
看例子:
interface Animal {
dateOfBirth: any;
}
interface Dog extends Animal {
breed: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
// Does not emit JavaScript code,
// only ensures the types are correct
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
Initialization Order(初始化顺序)
前面我们讲了很多class继承及class初始化的相关知识,那么大家应该也会好奇初始化相关的执行顺序到底是怎样的呢?
看个例子:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// Prints "base", not "derived"
const d = new Derived();
代码执行过程顺序如下:
1、基础类字段初始化
2、基础类构造函数执行
3、子类字段初始化
4、子类构造函数执行
Member Visibility(class属性及方法可见性)
你可以使用TS控制属性在方法在class外部的可见性。
public(公开)
看个例子:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
其实我们也可以省略public关键字,TS默认就是public。公开是权限完全放开的方式。
protected(受保护)
受保护的限制表示只能在子类中使用父类的属性和方法,看例子:
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
//Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Exposure of protected members(将受保护的字段暴露出去)
我们可以在子类中改写父类的区域限制:
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
private(私有)
私有化的属性不能被自身的实例化对象访问,也不能被子类继承。
不能在实例中访问的示例:
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
//Property 'x' is private and only accessible within class 'Base'.
不能在子类中被访问的示例1:
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
//Property 'x' is private and only accessible within class 'Base'.
}
}
不能在子类中访问的示例2:
class Base {
private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'.
Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
Static Members(静态成员)
class拥有不需要在实例化之后才能访问的属性和方法,这些成员就叫做静态成员。他们可以直接通过类名访问。
一个简单的例子:
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
静态类型也能用可见约束限制:
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
//Property 'x' is private and only accessible within class 'MyClass'.
静态成员也能被继承:
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Special Static Names(特殊的静态名)
因为类本质是构造函数,所以函数的自带属性不能作为类的静态名,例如:name, length, and call。
看个例子:
class S {
static name = "S!";
}
//Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
Why No Static Classes?(为什么不建议用static)
试想一下,咱们平时的函数调用和对象的方法调用是不是跟class的静态调用类似?
演示代码:
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}
// Preferred (alternative 1) 替代方案1
function doSomething() {}
// Preferred (alternative 2)替代方案2
const MyHelperObject = {
dosomething() {},
};
Generic Classes(范型class)
直接看简短的代码演示:
class Box {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b = new Box("hello!");
//const b: Box
this at Runtime in Classes(class在执行过程中的this指向)
ts的执行的行为跟js一致,比如this指向,我们先看一个列子:
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName());
如上例所示,getName返回的不是实例c的mame(MyClasss),而是返回的obj的name(obj),长话短说,在js中有个非常有趣的this指向特性,它会根据调用环境来判断到底this指向谁。例子中obj调用的getName,其完整的调用栈是obj->c.getName,其调用环境是obj,obj的name是“obj”,所以return this.name就是返回的obj字符。
然而对于我们一些新手js开发人员,我其实就想要实现返回实例c的name,那么我们可以怎么处理呢?
看解法1:
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
console.log(obj.getName());
箭头函数巧妙的帮我们改变了this指向。
那么还有什么思路可以帮我们规避非常规的调用呢?
看解法2:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// Error, would crash
const g = c.getName;
console.log(g());
//The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
这时候提示我们的调用context(上下文)this不符合getName中this设定的MyClass类型,也可以帮我们规避非常规的调用。
this Types(this的类型)
在class中,最特殊的类型就属this了,它会动态的推断成当前的class类型,请看如下示例:
class Box {
contents: string = "";
set(value: string) {
this.contents = value;
return this;
}
}
上述代码段,我们返回set的this。如果我们直接实例化Box,那么毋庸置疑返回的this就是Box类型。
这时候我写个继承类ClearableBox:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
const c = a.set("hello");
那么这时候的set返回的this类型就成了ClearableBox
官方的handbook关于类的翻译写的我是真累,到此整个handbook的精编版已经完成,后续我会找一篇TS的Class的文章继续作为内容补充,下篇预告:演绎篇