在系统学习 TypeScript 前,需要先部署开发环境,具体可以查看
基本类型
TypeScript 与 JavaScript 一样,常见的基本类型有:
let name: string = "张三";
console.log(name.toUpperCase());
let age: number = 42;
console.log(age.toFixed(2));
let isOk: boolean = true;
console.log(isOk.valueOf());
这里的用注解的方式进行类型声明,这并不是必须的,在这里,TypeScript 的类型检查器可以很容易地推断出局部变量的类型,即使它们没有使用注解标记类型。
数组
数组类型可以使用<type>[]表示,比如:
let list: number[] = [1, 2, 3];
console.log(list.length);
也可以使用泛型Array<type>表示:
let list2: Array<number> = [1, 2, 3];
console.log(list2.length);
它们是等效的。
any
any 类型表示这可以是任何一个类型,换言之,TypeScript 的类型检查器将失效,比如:
let obj: any = { x: 0 };
console.log(obj.x);
console.log(obj.y); //undefined
console.log(obj.toUpperCase()); // TypeError: obj.toUpperCase is not a function
因为这里obj被声明为any,所以可以使用任意的方式使用它,都不会报错(无类型检查)。
这是有意义的,对于某些从 JavaScript 移植到 TypeScript 的项目,或者某个需要编写繁杂的类型定义才能通过检查的变量,你都可以使用any简单地绕过这些类型检查。
正如前面所说,TypeScript 会在没有显式指定类型时,尽可能地自动推断出可能的类型。但某些时候无法通过上下文推断出类型,此时就会将目标类型视为any。这在大多数时候不是问题,但如果你认为这样不合适,应当在这样的情况下人为补全类型说明,而非使用这种行为。可以在这种情况下使用编译器标识noImplicitAny关闭这一行为。
函数
参数类型
function printMsg(msg: string){
console.log(msg);
}
printMsg("hello world");
TypeScript 支持可选参数:
function total(a: number, b?: number) {
if (b === undefined) {
b = 0;
}
return a + b;
}
console.log(total(1));
console.log(total(1, 2));
这里b?表示这个参数可传可不传,如果不传的时候这里接收到的就是undefined,因此函数中需要检查对应的实参值是否是undefined,否则会报错。
返回值类型
function randomNumber(): number {
return Math.random();
}
console.log(randomNumber());
返回值类型声明并非必须的,通常 TypeScript 都可以通过return的类型判断出函数的返回值类型。
对象类型
可以简单地使用字面量的方式定义目标对象的类型:
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: 100, y: 100 });
对象类型中的属性类型是可选的,如果没有指定,默认是any。
可选属性
function printName(obj: { first: string; last?: string }) {
if (obj.last !== undefined) {
console.log(obj.first + " " + obj.last);
} else {
console.log(obj.first);
}
}
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
与可选参数类似,这里同样需要判断可选属性是否是undefined。
联合类型
在 TypeScript 中,可以使用联合(Union)的方式定义一个拥有多个值/类型的类型。
一种 ID,可以是数字或字符串:
type ID = number | string;
function getID(id: ID): string {
return `ID: ${id}`;
}
console.log(getID(123));
console.log(getID("abc"));
自定义 bool 类型:
type MyBoolean = true | false;
function isTrue(value: MyBoolean): string {
return value ? "真" : "假";
}
console.log(isTrue(true));
console.log(isTrue(false));
枚举类型:
type WindowStates = "open" | "closed" | "minimized";
function getWindowState(state: WindowStates): string {
return `当前窗口状态为:${state}`;
}
console.log(getWindowState("open"));
console.log(getWindowState("closed"));
console.log(getWindowState("minimized"));
字符串或数组:
function wrapInArray(obj: string | string[]) {
if (typeof obj === "string") {
return [obj];
}
return obj;
}
console.log(wrapInArray("hello"));
console.log(wrapInArray(["hello"]));
可以看到,TypeScript 的类型与传统的强类型语言中的类型完全不同,它的类型可以被看作一个类型的集合,因此可以是类型 A 也可以是类型 B。
缩小范围
使用了联合类型,意味着接收到的类型可能是其中的任何一个类型,换言之,你只能使用这些可能类型都拥有的属性或方法。如果你要使用特定的方法,就需要先通过类型判断缩小范围:
function printId(id: number | string) {
if (typeof id === "string") {
id = id.toUpperCase();
}
console.log("Your id is: " + id);
}
printId(101);
printId("abc-123"); // Your id is: ABC-123
类型别名
可以将任意类型使用类型别名表示:
type Student = { name: string; age: number };
function printStudent(student: Student) {
console.log("Name: " + student.name);
console.log("Age: " + student.age);
}
printStudent({ name: "张三", age: 18 });
interface
除了使用type,interface同样可以用于定义类型别名:
interface Student2 {
name: string;
age: number;
}
function printStudent2(student: Student2) {
console.log("Name: " + student.name);
console.log("Age: " + student.age);
}
printStudent2({ name: "张三", age: 18 });
type or interface
与类型别名不同,interface可以重复定义的方式进行扩展,比如:
interface Student3 {
name: string;
}
interface Student3 {
age: number;
}
function printStudent3(student: Student3) {
console.log("Name: " + student.name);
console.log("Age: " + student.age);
}
看起来多少有点奇怪,如果用类似的方式使用type定义,就会报错。
当然这样的写法并不常见,更常见的是通过继承的方式扩展接口,比如:
interface Person {
name: string;
age: number;
}
interface Teacher extends Person {
teach(): void;
}
如果用类似的方式扩展type:
type Person2 = { name: string; age: number };
type Teacher2 = Person2 & { teach(): void };
类型断言
在浏览器中使用document.getElementById获取到的 DOM 元素的类型都是HTMLElement,但如果我们直到目标 DOM 的具体类型:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
这有点像 Java 中的强制类型转换,我们缩小了类型范围。
除了这种写法,还可以使用类似泛型的写法:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
为了避免出现奇怪的写法,类型断言仅能将类型变得更具体或更宽泛,不能将其用于两个没有关系的类型:
const x = "hello" as number;
这里 string 和 number 是没有任何关系的两个类型,因此不能使用类型断言进行转换。
但有时候你或许会希望绕过这种限制,强行将类型指定为某个类型,可以通过下面的方式:
const a = expr as any as T;
这里先将类型指定为any,再指定为目标类型。
字面量类型
type WindowStates2 = "open" | "closed" | "minimized";
function setWindowState(state: WindowStates2) {
console.log(state);
}
setWindowState("open");
setWindowState("closed");
字面量类型可以和普通类型结合使用:
type Configuration = {
readonly databaseURL: string;
readonly maxConnections: number;
};
function setConfig(configuration: Configuration | "auto") {
let databaseURL: string;
let maxConnections: number;
if (configuration === "auto") {
databaseURL = "https://example.com/db";
maxConnections = 42;
} else {
databaseURL = configuration.databaseURL;
maxConnections = configuration.maxConnections;
}
console.log(`databaseURL: ${databaseURL}`);
console.log(`maxConnections: ${maxConnections}`);
}
setConfig({ databaseURL: "https://example.com/mydb", maxConnections: 111 });
setConfig("auto");
字面量推断
对象的属性可能在任意时候被修改,比如:
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
因此这里 TypeScript 会将对象obj的counter属性的类型推断为number,而不是字面量类型0。
这在某些时候会出现问题,比如:
function handleRequest(url: string, method: "GET" | "POST"){
console.log(`URL: ${url}`);
console.log(`Method: ${method}`);
}
handleRequest("https://example.com", "GET");
这样没有任何问题,但是如果使用对象属性传参:
function handleRequest(url: string, method: "GET" | "POST"){
console.log(`URL: ${url}`);
console.log(`Method: ${method}`);
}
const request = {
url: "https://example.com",
method: "GET"
}
handleRequest(request.url, request.method); // 类型 “string” 的参数不能赋给 类型 “"GET" | "POST"” 的参数。
这里会报错,报错信息见注释。可能这里的报错会让人困惑,明明此时request.method的值就是GET。
但就像前面所说,对象的属性值是可以随时修改的,所以这里 TypeScript 的类型推断仅仅会人为它是string类型,而不认为它的值固定在GET。
可以强行指定类型的方式解决这个问题:
handleRequest(request.url, request.method as "GET");
表示“我很确定此时的值就是GET”。
或者:
const request = {
url: "https://example.com",
method: "GET"
} as const;
handleRequest(request.url, request.method);
这表示我确定request的属性值不会再改变,因此它的类型应当是字面量类型。
strictNullChecks
开启编译器选项strictNullChecks后,当类型可能是null或undefined时,需要在判断排除后才能调用相应的属性/方法:
function liveDangerously(x?: number | null) {
if (x !== null && x !== undefined) {
console.log(x.toFixed());
}
}
liveDangerously(null);
liveDangerously(undefined);
liveDangerously();
liveDangerously(1);
如果确定目标类型不是null或undefined,可以:
function liveDangerously2(x?: number | null) {
console.log(x!.toFixed());
}
这里的x!是一种断言,表示x必然存在(不是null或undefined)。
泛型
一个典型用途是定义特定类型元素的数组,比如字符串数组:
type StrArray = Array<string>;
function printStrArray(arr: StrArray) {
arr.forEach(item => {
console.log(item);
});
}
printStrArray(["hello", "world"]);
数字数组:
type NumberArray = Array<number>;
function getTotal(arr: NumberArray) {
return arr.reduce((acc, cur) => acc + cur);
}
console.log(getTotal([1, 2, 3, 4, 5]));
自定义类型也可以使用泛型:
interface Result<T> {
data: T;
errorMsg: string | undefined;
success: boolean;
toString: () => string;
isSuccess: () => boolean;
setData: (data: T) => void;
getData: () => T | null;
}
class MyResult<T> implements Result<T> {
data: T;
errorMsg: string | undefined;
success: boolean;
constructor(data: T, success: boolean, errorMsg?: string) {
this.data = data;
this.success = success;
this.errorMsg = errorMsg;
}
toString() {
return `data: ${this.data}, success: ${this.success}, errorMsg: ${this.errorMsg}`;
}
// 实现 isSuccess 方法
isSuccess(): boolean {
return this.success;
}
// 实现 setData 方法
setData(data: T): void {
this.data = data;
}
// 实现 getData 方法
getData(): T | null {
return this.success ? this.data : null;
}
}
const result: Result<number> = new MyResult<number>(123, true);
console.log(result.toString());
结构化类型系统
TypeScript 使用一种“鸭子类型”进行验证,也就是说只要接收的类型与定义的类型能匹配,就可以通过类型验证,并不需要接收的类型被显式定义为目标类型:
interface Point{
x: number;
y: number;
}
function printPoint(point: Point) {
console.log(`x: ${point.x}, y: ${point.y}`);
}
printPoint({x: 1, y: 2});
值得注意的是,如果是字面量,就需要严格匹配:
printPoint({x: 1, y: 2});
// printPoint({x: 1});
// printPoint({x: 1, y: 2, z: 3});
这里字面量无论是少属性还是多属性都无法通过验证。
反之,如果是对象,就比较宽泛,只要目标对象能满足验证条件即可:
const point3 = { x: 12, y: 26, z: 89 };
printPoint(point3); // 打印 "12, 26"
这里多属性依然能通过验证。
这样设计是说的通的,毕竟字面量仅仅用于这里,并不会在其他地方复用,严格要求很合理。
对于类,也是一样的,满足验证条件即可,并不需要显式让类实现目标类型:
class MyPoint{
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point = new MyPoint(1, 2);
printPoint(point);
看一个更极端的例子:
interface Empty{}
function printEmpty(empty: Empty) {
console.log("empty");
}
printEmpty({x: 1, y: 2});
因为类型Empty没有任何属性,因此这里任何对象都能满足类型检查。
再次强调,在鸭子类型中,只要两个类型长得像,它们就可以被看作同一种类型,即使没有在任何地方声明它们之间的连系:
class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
// No error?
let w: Car = new Golfer();
The End.

文章评论