PlayGround 快速体验
使用TypeScript 要达成什么目标?
利用 TypeScript 编写易于维护的代码,小幅增加开发成本,大幅降低维护成本
JavaScript 的常见报错
隐式转换
1 + '1',1 * [31],1 + [31]
let a = 1
let b = '1'
let c = a + b
// c 是字符串拼接。但是有没有可能作者是想执行数字相加?你得联系上下文、xxxx 各种考虑
js推荐使用显式转换
函数参数
无参数检验,可能产生参数数量,类型不兼容。
调用对象上不存在的方法
xxxx of undefined
TypeScript VS JavaScript
类型系统
JavsScript 运行时推导类型
C/Java 显式注解类型
TypeScript 显式注解 + 编译时推导
let a: number = 1 // number
let b = 1 // number (推导)
类型自动转换
JavaScript 类型对用户抽象
//JavaScript 发现 3 是一个数字,[1] 是一个数组。
//由于使用的是 +,JavaScript 假定你是想拼接二者。
//JavaScript 把 3 隐式转换为字符串,得到“3”
//JavaScript 把[1]隐式转换为宇符串,得到“1"
//把两个字符申拼接在一起,得到“31"
3 + [1] // 隐式转换 。 "31"
//更应该明确表示你的意图
(3).toString() + [1].toString() // "31"
TypeScript 类型对用户开放
// Operator '+' cannot be applied to types 'number' and 'number[]'.
3 + [1]
// 必须明确表示你的意图
(3).toString() + [1].toString() // "31"
报错时机
JavaScript 在运行时报错
TypeScript 在编译时报错
类型 (Types)
JavaScript 动态类型
JavaScript 是一种有着动态类型的动态语言。JavaScript 中的变量与任何特定值类型没有任何关联,并且任何变量都可以分配(重新分配)所有类型的值:
developer.mozilla.org/zh-CN/docs/… 截图
TypeScript 静态类型
强类型的核心:
对 T 类型的值来说,我们不仅知道值的类型是 T,还知道可以或不可以对该值做什么操作
图:www.typescriptlang.org/docs/handbo…
图: TypeScript 类型关系
简单类型
null
同javascript
let z: null = null;
undefined
同javascript
let y: undefined = undefined;
boolean
同javascript
// example
let a = true // boolean TypeScript 推导出值的类型为 boolean
let b = false // boolean TypeScript 推导出值的类型为 boolean
const c = true // true 使用 const,让 TypeScript 推导出值为某个具体的布尔值
let d: boolean = true // boolean 显式注解,声明值的类型为 boolean
let e: true = true // true 显式注解,声明值为某个具体的布尔值。类型字面量
let f: true = false // Error TS2322: Type 'false' is not assignable to type 'true'.
number
同javascript
// example
let a = 1234 // number TypeScript 推导出值的类型为 number
let b = Infinity * 0.1 // number TypeScript 推导出值的类型为 number
const c = 5678 // 5678 使用 const,让 TypeScript 推导出值为某个具体的数字
let d = a < b // boolean TypeScript 推导出值的类型为 boolean
let e: number = 100 // number 显式注解,声明值的类型为 number
let f: 26.218 = 26.218 // 26.218 显式注解,声明值为某个具体的数字
let g: 26.218 = 10 // Error TS2322: Type '10' is not assignable to type '26.218'
处理较长的数字时可以使用数字分隔符
let oneMillion = 1_000_ 000 // 等同于 1000000
let twoMillion: 2_000_000 = 2_000_000
bigint
同javascript
number 类型表示的整数最大为 2(53),bigint 可以表示任意大的整数。
// example
let a = 1234n // bigint
const b = 5678n // 5678n
let c = a + b // bigint
let d = a < 1235 // boolean
let e = 88.5n // Error TS1353: A bigint literal must be an integer.
let f: bigint = 100n // bigint
let g: 100n = 100n // 100n
let h: bigint = 100 // Error TS2322: Type '100' is not assignable ty type 'bigint'.
与 boolean 和 number 一样,声明 bigint 类型也有四种方式。尽量让 TypeScript 自动推导。
string
同javascript
// example
let a = 'hello' // string
let b = 'billy' // string
const c = '!' // !
let d = a + ' ' + b + c // string
let e: string = 'zoom' // string
let f: 'john' = 'john' // john
let g: 'john' = 'zoe' // Error TS2322: Type 'zoe' is not assignable to type 'john'
同样也是尽量让 TypeScript 自动推导 string 类型。
symbol
同javascript
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
其他类型
Object
示例
const car: { type: string, model: string, year: number } = {
type: "Toyota",
model: "Corolla",
year: 2009
};
类型推断
const car = {
type: "Toyota",
};
car.type = "Ford"; // no error
car.type = 2; // Error: Type 'number' is not assignable to type 'string'.
可选参数
const car: { type: string, mileage: number } = {
// Error: Property 'mileage' is missing in type '{ type: string; }' but required in type '{ type: string; mileage: number; }'.
type: "Toyota",
};
car.mileage = 2000;
const car: { type: string, mileage?: number } = {
// no error
type: "Toyota"
};
car.mileage = 2000;
索引签名
const nameAgeMap: { [index: string]: number } = {};
nameAgeMap.Jack = 25; // no error
nameAgeMap.Mark = "Fifty"; // Error: Type 'string' is not assignable to type 'number'.
// 后面会讲到间接写法:Record<string, number>
Array
const names: string[] = [];
names.push("Dylan"); // no error
names.push(3); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.
names.length // number 类型与元组不同
类型推断
const numbers = [1, 2, 3]; // inferred to type number[]
numbers.push(4); // no error
// comment line below out to see the successful assignment
numbers.push("2"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
let head: number = numbers[0]; // no error
只读
const names: readonly string[] = ["Dylan"];
names.push("Jack"); // Error: Property 'push' does not exist on type 'readonly string[]'.
// try removing the readonly modifier and see if it works? it work
Tuple
array 的子类型,长度固定,各索引位上的值具有固定的已知类型。
What-is-a-tuple
长度不变
// define our tuple
let ourTuple: [number, boolean, string];
// initialize correctly
ourTuple = [5, false, 'Coding God was here'];
// initialized incorrectly which throws an error
ourTuple = [false, 'Coding God was mistaken', 5];
ourTuple.length // 类型是 3 ,与数组不同
// 报错: Tuple type '[number, boolean, string]' of length '3' has no element at index '4'.
console.log(ourTuple[4])
只读
// define our readonly tuple
const ourReadonlyTuple: readonly [number, boolean, string] = [5, true, 'The Real Coding God'];
// throws error as it is readonly.
ourReadonlyTuple.push('Coding God took a day off');
命名
let person: [ age: number, firstName: string ];
person = [ 26, 'Tim' ];
// react useState hook 举例
const [firstName, setFirstName] = useState('Dylan')
// firstName: string
// setFirstName: function
Union Types (联合类型)
TypeScript 的类型系统使用多种运算符从现有类型构建新类型。
当一个值可以有多个类型时,可以使用联合类型。
type Cat = { name: string, purrs: boolean }
type Dog ={ name: string, barks: boolean, wags: boolean }
type CatOrDogOrBoth = Cat | Dog // 并集
// CatOrDogOrBoth 可以是 Cat 类型的值,可以是 Dog 类型的值,还可以二者兼具。
// Cat
let a: CatOrDogOrBoth = {
name: 'Bonkers',
purrs: true
}
// Dog
a = {
name: 'Domino',
barks: true,
wags: true
}
// 二者兼具
a = {
name: 'Donkers',
barsk: true,
purrs: true,
wags: true
}
// number | string 联合类型
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'
printId({ myID: 22342 });
Intersection Types (交叉类型)
type Cat = { name: string, purrs: boolean }
type Dog ={ name: string, barks: boolean, wags: boolean }
type CatAndDog = Cat & Dog // 交集
// CatAndDot
let b: CatAndDog = {
name: 'Domino',
barks: true,
purrs: true,
wags: true
}
Literal Types (字面量类型)
let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString; // 推导出 string,因为可变
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString; // 推导出 "Hello World" 类型,因为不可变
// 示例
let x: "hello" = "hello";
// OK
x = "hello";
// error Type '"howdy"' is not assignable to type '"hello"'.
x = "howdy";
// 更常用场景
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
// error Argument of type '"center"' is not assignable to parameter of type '"left" | "right" | "center"'.Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
printText("G'day, mate", "center");
Function
参数注解
// Parameter type annotation
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
// Would be a runtime error if executed!
greet(42);
// Argument of type 'number' is not assignable to parameter of type 'string'.
返回值注解
TypeScript 会根据 return 语句自动推断出返回值类型。
官方不推荐显示声明。(出于文档目的或个人偏好可声明,不会影响运行时)
function getFavoriteNumber(): number {
return 26;
}
匿名函数
没必要显示声明匿名函数的类型,TypeScript 会根据上下文推断出函数的参数类型。
const names = ["Alice", "Bob", "Eve"];
// Contextual typing for function - parameter s inferred to have type string
names.forEach(function (s) {
console.log(s.toUpperCase());
});
// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUpperCase());
});
enum
What can I do with an enum variable?
stackoverflow.com/questions/2…
How Does An Enum Work In TypeScript?
数字枚举
enum CardinalDirections {
North,
East,
South,
West
}
let currentDirection = CardinalDirections.North;
// logs 0
console.log(currentDirection);
// throws error as 'North' is not a valid enum
currentDirection = 'North'; // Error: "North" is not assignable to type 'CardinalDirections'.
enum CardinalDirections {
North = 1,
East,
South,
West
}
// logs 1
console.log(CardinalDirections.North);
// logs 4
console.log(CardinalDirections.West);
enum StatusCodes {
NotFound = 404,
Success = 200,
Accepted = 202,
BadRequest = 400
}
// logs 404
console.log(StatusCodes.NotFound);
// logs 200
console.log(StatusCodes.Success);
字符串枚举
enum CardinalDirections {
North = 'North',
East = "East",
South = "South",
West = "West"
};
// logs "North"
console.log(CardinalDirections.North);
// logs "West"
console.log(CardinalDirections.West);
不推荐字符和数字混合。
反向映射
enum Enum {
A,
}
let a = Enum.A; // a = 0
let nameOfA = Enum[a]; // "A"
特别类型
any
当一个值是any类型时,你可以访问它的任何属性(反过来将是any类型),像函数一样调用它,将其分配给(或从)任何类型的值,或者几乎任何其他语法上合法的东西:
当您不指定类型并且 TypeScript 无法从上下文中推断出类型时,编译器通常会默认为 any。
let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
unknown
描述这样一种情况“这可以是任何值,因此您必须在使用它之前执行某种类型的检查”。
用来强制用户自检返回的值。
let vAny: any = 10; // We can assign anything to any
let vUnknown: unknown = 10; // We can assign anything to unknown just like any
let s1: string = vAny; // Any is assignable to anything
let s2: string = vUnknown; // Invalid; we can't assign vUnknown to any other type (without an explicit assertion)
vAny.method(); // Ok; anything goes with any
vUnknown.method(); // Not ok; we don't know anything about this variable
interface Auth {
accessToken: string;
};
function isAuth(value: unknown): value is Auth {
if (typeof value != "object") {
throw new Error(`Unexpected type: ${typeof value}`);
}
if (value == null) {
throw new Error("Unexpected value: null");
}
if (!("accessToken" in value)) {
throw new Error("Missing property: accessToken");
}
if (typeof value.accessToken != "string") {
throw new Error(`Unexpected type: ${typeof value.accessToken}`);
}
return true;
}
// then, say you have an unknown value and you want to check if it is an Auth
declare const x: unknown;
try {
if(isAuth(x)) {
// x type is refined as Auth in here
x.accessToken;
}
} catch(e) {
// ...
}
unknown 和 any 在语义上不同
Unknown 是所有其他类型的父类型。它是类型系统中的常规类型。
any 表示“禁用类型检查”。这是一个编译器指令。
void
没有返回 "value"
// The inferred return type is void
function noop() {
return;
}
never
永不返回
// 返回 never 的函数
function d() {
throw TypeError('I always error')
}
function e() {
while (true) {
doSomething()
}
}
never 是所有类型的子类型,可以赋值给其他任何类型。
类型别名(Type Aliases)
类型别名:允许使用自定义名称表示类型。 (an Alias).
类似变量和值, let a = 1
type CarYear = number
type CarType = string
type CarModel = string
type Car = {
year: CarYear,
type: CarType,
model: CarModel
}
const carYear: CarYear = 2001
const carType: CarType = "Toyota"
const carModel: CarModel = "Corolla"
const car: Car = {
year: carYear,
type: carType,
model: carModel
};
www.typescriptlang.org/static/Type…
接口 (interface)
接口类似于类型别名,只是它们仅适用于对象类型。
interface Rectangle {
height: number,
width: number
}
const rectangle: Rectangle = {
height: 20,
width: 10
};
www.typescriptlang.org/static/Type…
类型别名和接口之间的差异
type 和 interface 都可以用来描述对象类型,有什么不同?
类型操作
TypeScript 类型自身可以执行一些操作。
keyof
keyof是TypeScript中的一个关键字,用于从对象类型中提取键类型。
interface Person {
name: string;
age: number;
}
// `keyof Person` here creates a union type of "name" and "age", other strings will not be allowed
function printPersonProperty(person: Person, property: keyof Person) {
console.log(`Printing person property ${property}: "${person[property]}"`);
}
let person = {
name: "Max",
age: 27
};
printPersonProperty(person, "name"); // Printing person property name: "Max"
typeof
JavaScript 已经有一个可以在表达式上下文中使用的 typeof 运算符
// Prints "string"
console.log(typeof "Hello world");
TypeScript 添加了一个 typeof 运算符,您可以在类型上下文中用于引用变量或属性的类型
let s = "hello";
let n: typeof s; // string
// 常用于类型操作
function f() {
return true
}
// f 是值。typeof f 得到 f 的类型
type P = ReturnType<typeof f>; // boolean
Utility Types
文档: www.typescriptlang.org/docs/handbo…
重点介绍几个
Record
使用键类型和值类型定义对象类型 (同对象索引类型)
const nameAgeMap: Record<string, number> = {
'Alice': 21,
'Bob': 25
};
Partial
将对象中所有属性变成可选
interface Point {
x: number;
y: number;
}
let pointPart: Partial<Point> = {}; // `Partial` allows x and y to be optional
pointPart.x = 10;
Required
将对象中所有属性变成必须
interface Car {
make: string;
model: string;
mileage?: number;
}
let myCar: Required<Car> = {
make: 'Ford',
model: 'Focus',
mileage: 12000 // `Required` forces mileage to be defined
};
Omit
去除对象中的属性
interface Person {
name: string;
age: number;
location?: string;
}
const bob: Omit<Person, 'age' | 'location'> = {
name: 'Bob'
// `Omit` has removed age and location from the type and they can't be defined here
};
Pick
挑选对象中的属性
interface Person {
name: string;
age: number;
location?: string;
}
const bob: Pick<Person, 'name'> = {
name: 'Bob'
// `Pick` has only kept name, so age and location were removed from the type and they can't be defined here
};
ReturnType
提取函数的返回类型
type PointGenerator = () => { x: number; y: number; };
const point: ReturnType<PointGenerator> = {
x: 10,
y: 20
};
Parameters
以数组的形式提取函数的参数类型
type PointPrinter = (p: { x: number; y: number; }) => void;
const point: Parameters<PointPrinter>[0] = {
x: 10,
y: 20
};
泛型
泛型允许创建“类型变量”,这些变量可用于创建类、函数和类型别名,而不需要显式定义它们使用的类型。
下面是一个,identity 函数,它返回传入的内容。
函数
// 思考:如何表达一个函数的返回值的类型和参数的类型一致?
// 传入 number 返回 number
// 传入 string 返回 string
// number
function identity(arg: number): number {
return arg;
}
// any
function identity(arg: any): any {
return arg;
}
// generics
function identity<T>(arg: T): T{
return arg;
}
let output = identity<string>("myString"); // string
// 推荐简便写法 ,泛型参数同样可以做类型推导。
let output = identity("myString"); // string
类
class NamedValue<T> {
private _value: T | undefined;
constructor(private name: string) {}
public setValue(value: T) {
this._value = value;
}
public getValue(): T | undefined {
return this._value;
}
public toString(): string {
return `${this.name}: ${this._value}`;
}
}
let value = new NamedValue<number>('myNumber');
value.setValue(10);
console.log(value.toString()); // myNumber: 10
类型别名
type Wrapped<T> = { value: T };
const wrappedValue: Wrapped<number> = { value: 10 };
泛型约束
思考下面这个函数,如果用 javascript 要怎么做~?
// 泛型表示 arg 参数的类型不固定
function loggingIdentity<Type>(arg: Type): Type {
// Property 'length' does not exist on type 'Type'.Property 'length' does not exist on type 'Type'.
console.log(arg.length);
return arg;
}
如何用 typescript 的思维解决?借助类型:
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
函数
返回值
返回值类型
// the `: number` here specifies that this function returns a number
function getTime(): number {
return new Date().getTime();
}
通常不显式声明。自动推导
void 返回值类型
function printHello(): void {
console.log('Hello!');
}
参数
参数类型
function multiply(a: number, b: number) {
return a * b;
}
可选参数
// the `?` operator here marks parameter `c` as optional
function add(a: number, b: number, c?: number) {
return a + b + (c || 0);
}
可选参数必须在末尾
默认参数
function pow(value: number, exponent: number = 10) {
return value ** exponent;
}
默认参数更常用,默认参数可以自动类型推导。
剩余参数
function add(a: number, b: number, ...rest: number[]) {
return a + b + rest.reduce((p, c) => p + c, 0);
}
解构参数
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
// Same as prior example
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
声明this
function fancyDate(this: Date) {
return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`;
}
call,apply 调用时有用。
函数重载
首选使用联合类型的参数,而不是函数重载。
重载版本
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
联合类型版本
// 更简洁
function len(x: any[] | string) {
return x.length;
}
类
在 JavaScript 之上加了拓展。
访问修饰符
-
public- (默认) 当前类、子类、实例可访问 -
protected- 当前类、子类、可访问 -
private- 当前类可访问
只读成员
class Person {
private readonly name: string;
public constructor(name: string) {
// name cannot be changed after this initial definition, which has to be either at it's declaration or in the constructor.
this.name = name;
}
public getName(): string {
return this.name;
}
}
const person = new Person("Jane");
console.log(person.getName());
参数属性
TypeScript 提供了特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
console.log(a.z);
继承
interface
interface Shape {
getArea: () => number;
}
class Rectangle implements Shape {
public constructor(
protected readonly width: number,
protected readonly height: number
) {}
public getArea(): number {
return this.width * this.height;
}
}
class Rectangle implements Shape, Colored {}
extends
interface Shape {
getArea: () => number;
}
class Rectangle implements Shape {
public constructor(
protected readonly width: number,
protected readonly height: number
) {}
public getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
public constructor(width: number) {
super(width, width);
}
}
// getArea gets inherited from Rectangle
抽象类
// 抽象类
abstract class Polygon {
// 抽象方法
public abstract getArea(): number;
public toString(): string {
return `Polygon[area=${this.getArea()}]`;
}
}
class Rectangle extends Polygon {
public constructor(
protected readonly width: number,
protected readonly height: number
) {
super();
}
// 子类必须实现抽象方法
public getArea(): number {
return this.width * this.height;
}
}
装饰器
JavaScript 提案,Stage 3 (Candidate)
TypeScript 文档
devblogs.microsoft.com/typescript/…
有一个 Person 类,里面有个 greet 方法。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
要实现进入函数体和离开函数体时都打印信息。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}
这个经常需要用到,怎么优雅提取?
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
logEnter() // 侵入业务代码
console.log(`Hello, my name is ${this.name}.`);
logOut() // 侵入业务代码
}
}
提取成函数
function loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
作为装饰器使用
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
// Output:
//
// LOG: Entering method.
// Hello, my name is Ron.
// LOG: Exiting method.
参考资料
JavaScript 数据类型和数据结构
developer.mozilla.org/zh-CN/docs/…
Programming Typescript
books-library.net/files/books…
TypeScript 使用手册
www.typescriptlang.org/docs/handbo…
深入理解 TypeScript
用集合的方式看待 TypeScript
timmousk Typescript 系列文章
w3schools TypeScript Tutorial