TypeScript只是带有文档的JavaScript, TypeScript让JavaScript更美好, 学习JavaScript仍然是必要的
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
类型声明空间包含用来当做类型注解的内容,它被用作 类型注解 使用,不能当作变量使用
class Foo {}
interface Bar {}
type Bas = {};
// 可以当作类型注解使用:
let foo: Foo;
let bar: Bar;
let bas: Bas;
// 不能够把它作为一个变量来使用
interface Bar {}
const bar = Bar; // Error: "cannot find name 'Bar'"
变量声明空间包含可以用作变量的内容,class Foo提供了一个类型Foo到类型声明空间,此外还提供了一个变量到Foo到变量声明空间,因此下列语句可以正常通过TypeScript类型检查
const foo = Foo
但是我们不能把类似于 interface 定义的内容当作变量使用,同时变量也不能用作类型注解
const foo = 123;
let bar: foo; // ERROR: "cannot find name 'foo'"
默认情况下,当你在一个新的TypeScript文件中写下代码时,它处于全局命名空间中。如在foo.ts里的以下代码:
const foo = 123;
如果你在相同的项目里创建了一个新的文件bar.ts,TypeScript类型系统将会允许你使用变量foo,就好像它在全局可用一样:
const bar = foo; // allowed
毋庸置疑,使用全局变量空间是危险的,因为它会与文件内的代码命名冲突。我们强烈推荐使用文件模块。
它也被称为外部模块。如果在你的TypeScript文件的根级别位置含有import或者export,它会在这个文件中创建一个本地的作用域。因此,我们需要把上文foo.ts改成如下方式
export const foo = 1
防止变量泄露到全局, TypeScript提供了namespace关键字来描述这种分组
export namespace Utils {
export function log(msg: any) {
console.log(msg);
}
}
Utils.log('hello')
namespace也是支持嵌套的
const num: number = 123
function test(arg: string): void {
console.log(arg)
}
上面代码使用了变量注解,函数参数注解和函数返回值注解
const str: string = 'hello'
const boo: boolean = true
const num: number = 123
const ss: Symbol = Symbol('123')
const nn: undefined = undefined
const nn1: null = null
let array1:Array<number> = new Array<number>();
let array2:number[] = [1,2,3];
interface People {
name: string,
age: number,
hobby?: string
}
let p: People = {
name: 'david',
age: 23
}
let p: {
name: 'david',
age: 23
}
p = {
name: 'david'
}
let aa: any
aa = 123
aa = 'hello'
aa = true
当使用any的使用意味着告诉编辑器不要做任何类型检查,要尽量减少any的使用
let num = null
num = undefined
const hello = (): void => {
console.log('hello')
}
hello()
import { Utils } from "./utils";
// 1. 函数泛型
export function getIndexChild<T>(arr: T[], index: number): T {
return arr[index];
}
Utils.logAnything(getIndexChild(["a", "b", "c"], 1));
Utils.logAnything(getIndexChild([1, 2, 3], 2));
// 2. 泛型类
class Counter<T> {
a: any;
b: any;
constructor(a: T, b: T) {
this.a = a;
this.b = b;
}
getSum() {
if (typeof this.a === "number" && typeof this.b === "number") {
return this.a + this.b;
} else if (typeof this.a === "string" && typeof this.b === "string") {
return this.a.length + this.b.length;
}
}
}
const c1 = new Counter<number>(1, 2);
Utils.logAnything(c1.getSum());
const c2 = new Counter<string>("hello", "world");
Utils.logAnything(c2.getSum());
// 3. 泛型函数接口
export interface multiplyFun {
<T>(arr1: T[], arr2: T[]): T[];
}
const concat: multiplyFun = (arr1, arr2) => {
return arr1.concat(arr2);
};
Utils.logAnything(concat([1, 2, 3], [4, 5, 6]));
// 4. 泛型类接口
export interface Param {
[index: string]: any;
}
class Page {
private currentPage: number = 1; //当前页码 默认1
private pageSize: number = 10; //每页条数 默认为10
private sortName: string; //排序字段
private sortOrder: string = "asc"; // 排序规则 asc | desc 默认为asc正序
constructor(param: Param) {
if (param["currentPage"]) {
this.currentPage = param["currentPage"];
}
if (param["pageSize"]) {
this.pageSize = param["pageSize"];
}
if (param["sortName"]) {
this.sortName = param["sortName"];
}
if (param["sortOrder"]) {
this.sortOrder = param["sortOrder"];
}
}
public getStartNum(): number {
return (this.currentPage - 1) * this.pageSize;
}
}
interface User {
id: number; //id主键自增
name: string; //姓名
sex: number; //性别 1男 2女
age: number; //年龄
city: string; //城市
describe: string; //描述
}
//泛型接口
interface BaseDao<T> {
findById(id: number): T; //根据主键id查询一个实体
findPageList(param: Param, page: Page): T[]; //查询分页列表
findPageCount(param: Param): number; //查询分页count
save(o: T): void; //保存一个实体
update(o: T): void; //更新一个实体
deleteById(id: number); //删除一个实体
}
class UserDao<User> implements BaseDao<User> {
findById(id: number): User {
throw new Error("Method not implemented.");
}
findPageList(param: Param, page: Page): User[] {
throw new Error("Method not implemented.");
}
findPageCount(param: Param): number {
throw new Error("Method not implemented.");
}
save(o: User): void {
throw new Error("Method not implemented.");
}
update(o: User): void {
throw new Error("Method not implemented.");
}
deleteById(id: number) {
throw new Error("Method not implemented.");
}
}
const format = (word: string[] | string): string => {
let line = "";
if (typeof word === "string") {
line = word;
} else {
line = word.join("").trim();
}
return line;
};
console.log(format(["a", "b", "c"]));
const extend = <T, U>(first: T, second: U): T & U => {
const result = <T & U>{};
for (const id in first) {
(<T>result)[id] = first[id];
}
for (const id in second) {
// @ts-ignore
if (!result.hasOwnProperty(id)) {
(<U>result)[id] = second[id];
}
}
return result;
};
const x = extend({ a: "hello" }, { b: "world" });
console.log(x)
let arr: [number, string] = [0, '']
arr[0] = 123
arr[1] = 'hello'
console.log(arr)
export type strOrNum = string | number;
export type Text = string | { text: string }
export type Coordinates = [number, number]
export type Callback = (data: string) => void
一般来说,将JavaScript代码迁移至TypeScript包括以下步骤:
添加一个tsconfig.json文件。
把文件扩展名从.js改成.ts,开始使用any来减少错误。
开始在TypeScript中写代码,尽可能减少any的使用。
回到旧代码,开始添加类型注解,并修复已识别的错误。
为第三方JavaScript代码定义环境声明。(很多三方库的类型声明已经发布到了DefinitelyTyped中)
在TypeScript中可以允许导入任何文件,例如.css文件, 使用webpack的话只需要在globals.d.ts中添加如下代码 即可
declare module '*.css'
如果想使用HTML模板,如在Angular, 可以这样做
declare module '*.html'
毫无疑问,类型是TypeScript最大的优势之一,社区已经记录了将近90%的顶级JavaScript项目。这意味着,我们可以以交互式和探索性的方式来使用这些项目
yarn add @types/jquery --save-dev
@types支持全局和模块类型定义
在默认情况下,TypeScript会自动包含支持全局使用的任何定义。例如,对于jQuery,你应该能够在项目中全局使用$。
对于jQuery来说,通常建议使用模块。安装模块@types之后,不需要进行特别的配置,你就可以像使用模块一样使用它了。
import * as $ from 'jquery'
// 现在可以在次模块中任意使用$了
可以在tsconfig.json的compilerOptions.types选项,引入有意义的类型
{
"compilerOptions": {
"types": [
"jquery"
]
}
}
如上例所示,在配置compilerOptions.types:[“jquery”]之后,只允许使用jQuery的@types包。即使安装了另一个声明文件,如npm install@types/node,它的全局变量(如process)也不会泄漏到你的代码中,直到你将它们添加到tsconfig.json类型选项中。
你可以选择把这些声明放入**.ts或.d.ts里。在实际的项目中,强烈建议把声明放入独立的.d.ts里,你可以从一个命名为globals.d.ts或vendor.d.ts**的文件开始。
如果一个文件有扩展名**.d.ts**,这意味着每个根级别的声明都必须以declare关键字作为前缀。这可以让开发者清楚地知道,在这里,TypeScript不会把它编译成任何代码,同时,开发者需要确保所声明的内容在编译时存在。
声明文件编写参考
// declare const myPoint: { x: number; y: number };
interface Point {
x: number;
y: number;
}
declare const myPoint: Point
使用内联方式的类型定义和使用接口的方式定义是等效的,但是使用接口的方式便于其他使用者对其进行扩展
如在a.d.ts中有以下代码
interface Point {
x: number;
y: number;
}
在b.d.ts中有如下代码对接口Point进行了扩展
interface Point {
z: number;
}
如果你希望在类中使用必须要被遵循的接口(类)或别人定义的对象结构,可以使用implements关键字来确保其兼容性
基本上,在implements存在的情况下,外部Point接口的任何更改都将导致代码库中的编译错误,因此可以轻松地使其保持同步
class MyPoint implements Point {
x!: number;
y!: number;
z!: number;
}
如果接口Point的结构发生了变化,自然MyPoint也需要相应作出改变,否则就会报错
enum Color {
RED = 100,
GREEN = 200,
YELLOW = 300
}
console.log(Color.RED)
console.log(Color[100])
上面代码console.log的结果是因为上面的ts代码被编译后生成的js代码是这样的
var Color;
(function (Color) {
Color[Color["RED"] = 100] = "RED";
Color[Color["GREEN"] = 200] = "GREEN";
Color[Color["YELLOW"] = 300] = "YELLOW";
})(Color || (Color = {}));
console.log(Color.RED);
console.log(Color[100]);
Color[Color["RED"] = 100] = "RED";
这句代码会执行下述过程
Color["RED"] = 100 并返回 100 (在JavaScript中,赋值运算符返回的值是被赋予的值)
Color[100] = "RED"
用数字枚举做标记的一个实例
enum AnimalFlags {
None = 0,
HasClaws = 1 << 0, // 0000 0000
CanFly = 1 << 1, // 0000 0010
EatsFish = 1 << 2, // 0000 0100
FlyingClawedFishEating = HasClaws | CanFly | EatsFish // 0000 0001 || 0000 0010 || 0000 0100 0000 0111 -> 7
}
interface Animal {
flags: AnimalFlags;
[key: string]: any;
}
const printAnimalAbilities = (animal: Animal): void => {
var animalFlags = animal.flags;
if (animalFlags && animalFlags === AnimalFlags.HasClaws) {
console.log("有爪子的动物 ");
} else if (animalFlags && animalFlags === AnimalFlags.CanFly) {
console.log("会飞的动物");
} else if (animalFlags && animalFlags === AnimalFlags.EatsFish) {
console.log("吃鱼的动物");
} else if (
animalFlags &&
animalFlags === AnimalFlags.FlyingClawedFishEating
) {
console.log("万能的动物");
} else if (animalFlags == AnimalFlags.None) {
console.log("什么也不会的动物");
}
};
var animal: Animal = { flags: AnimalFlags.None }; // 0000 0000
animal.flags |= AnimalFlags.HasClaws // 0000 0000 || 0000 0001
animal.flags &= ~AnimalFlags.HasClaws // 0000 0001 & 1111 1110 0000 0000
animal.flags |= AnimalFlags.FlyingClawedFishEating // 0000 0000 || 0000 0111
printAnimalAbilities(animal);
enum MessageType {
SUCCESS = 'success',
ERROR = 'error',
WARN = 'warning'
}
console.log(MessageType.SUCCESS)
有时定义枚举可能只是为了让程序可读性更好,而不需要编译后的代码,即不需要编译成对象。typescript中考虑到这种情况,所以加入了 const enum
const enum Tristate {
False,
True,
Unknown
}
const lie = Tristate.False;
最终编译的js代码如下,并不会为枚举Tristate生成对应的Js代码
"use strict";
var lie = 0 /* False */;
//# sourceMappingURL=ConstEnum.js.map
如果需要让常量枚举也能编译到最终生成的JS代码中,可以使用preserveConstEnums编译选项
可以使用enum+namespace的声明方式向枚举类型添加静态方法。如下面的例子所示,我们将静态成员isBusinessDay添加到枚举上。
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
namespace Weekday {
export function isBusinessDay(day: Weekday){
switch (day) {
case Weekday.Saturday:
case Weekday.Sunday:
return false;
default:
return true;
}
}
}
const mon = Weekday.Monday;
const sun = Weekday.Sunday;
console.log(Weekday.isBusinessDay(mon))
console.log(Weekday.isBusinessDay(sun))
当你安装TypeScript时,会顺带安装一个lib.d.ts声明文件。这个文件包含JavaScript运行时及DOM(Document Object Model,文档对象模型)中存在的各种常见的JavaScript环境声明。
你可以通过指定**–noLib的编译器命令行标记,或者在tsconfig.json中指定选项noLib:true**,从上下文中排除此文件。
lib.d.ts的内容主要是一些变量声明,如window、document、math等,以及一些类似的接口声明,如Window、Document、Math。阅读全局内容的文档和类型注解的最简单的方法是输入你知道有效的代码,如Math.floor,然后在IDE上按F12键,跳转到定义上。
有时,你想要解耦编译目标(即生成的JavaScript版本)和环境库支持之间的关系。例如,对于Promise,你的编译目标是–target es5,但是你仍然想使用Promise,这时,你可以使用–lib选项来显示地控制lib。
注意:使用–lib选项可以将任何lib与–target解耦。
命令行
tsc --target es5 --lib dom,es6
tsconfig.json:
"compilerOptions": {
"lib": ["dom", "es6"]
}
● JavaScript功能
运行环境
ESNext功能选项
–lib选项提供了非常高效的控制,因此你最有可能从运行环境与JavaScript功能类别中分别选择一项。如果没有指定–lib,则会导入默认库。
函数类型在TypeScript类型系统中扮演着非常重要的角色,它们是可组合系统的核心构建块。
function func(arg: string| string[] | undefined | null) {
// do something
}
function func(arg: string| string[] | undefined | null): Result {
// do something
return {
data: [],
type: 'success'
}
}
interface Result {
data: any,
type: string
}
function func(arg: string| string[] | undefined | null, canBeNull?: number = 1): Result {}
// 函数重载
function print(a: number): void;
function print(a: number, b: number): void;
function print(a: number, b: number, c:number): void;
function print(a?: number, b?:number, c?: number): void {
if(!a && !b && !c ) return
if(a && !b && !c) console.log('a is: ' + a)
if(a && b && !c) console.log('a+b is: ' + (a+b))
if(a && b && c) console.log('a+b+c is: ' + (a+b+c))
};
print(1)
print(1,2)
print(1,2,3)
// 可调用
interface ReturnString {
(): string
}
// 它表示一个返回值为string的函数
declare const foo: ReturnString
const bar = foo()
一个实例
interface Complex {
(foo: string, bar?: number, ...others: boolean[]): number
}
interface Overloaded {
(foo: string): string;
(foo: number): number;
}
function stringOrNumber(foo: number): number;
function stringOrNumber(foo: string): string;
function stringOrNumber(foo: any): any {
if(typeof foo === 'number'){
return foo * foo;
} else if (typeof foo === 'string'){
return `hello ${foo}`;
}
}
const overloaded: Overloaded = stringOrNumber
const str = overloaded('') // str被推断为string类型
const num = overloaded(123) // num 被推断为number类型
console.log(str)
console.log(num)
function create<T>(c: {new(): T; }): T {
return new c();
}
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
TypeScript允许你以任何方式去重写其推断和分析的类型。这是通过一种被称为“类型断言”的机制来实现的。TypeScript类型断言纯粹是用来告诉编译器,你比它更了解这个类型,并且它不应该再发出错误提示。
类型断言的一个常见示例是将代码从JavaScript迁移到TypeScript。
const foo = {}
foo.bar = 123
这里的代码发出了错误警告,因为foo的类型推断为{},即具有零属性的对象。因此,你不能在它的属性上添加bar或bas,你可以通过类型断言来避免此问题。
const foo = {} as any
foo.bar = 123
console.log(foo)
也可以这样进行类型断言
const foo = <any>{}
foo.bar = 123
console.log(foo)
但是因为JSX中JS和XML混写,因此推荐使用as来进行类型断言
类型断言之所以不被称为“类型转换”,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也为编译器提供了分析代码的方法。
我们可以也可以利用类型断言来提供代码提示
interface People {
name: string;
age: number;
hobby: string;
}
const person = <People>{
name: 'david',
age: 23,
hobby: 'programming'
}
console.log(person)
这也会存在一个同样的问题,如果你忘了某个属性,编译器同样也不会发出错误警告。那么,像这样可能会更好一些。
const person1: People = {
name: 'tom',
age: 23
}
尽管我们已经证明了类型断言并不那么安全,但它也有用武之地。下面就是一个非常实用的例子,当使用者了解传入参数更具体的类型时,类型断言就能按预期工作。
const handler = (event: Event) => {
const mouseEvent = event as MouseEvent
}
对于对象字面量的类型,TypeScript 有一个被称之为 「Freshness 」的概念,它也被称为更严格的对象字面量检查
function logName(something: {name?: string}){
console.log(something.name)
}
const p1 = {name: 'david', job: 'programming'}
// 结构类型很方便,但是, 如果它允许传入并没有用到的属性
logName(p1)
// 对于对象字面量会进行Fressness检查(更为严格的对象字面量检查)
logName({name: 'jack', job: 'backendprogrammer'})
// 可以通过添加索引签名来允许字面量对象传递没有显示声明的属性
function logName1 (something: {name?: string, [x:string]: any}) {
console.log(something.name)
}
logName1({name: 'jack', job: 'backendprogrammer'})
在 TypeScript 中,typeof 操作符可以用来获取一个变量或对象的类型。
interface Person {
name: string;
age: number;
}
const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person
typeof 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
class Foo {
foo = 123;
common = '123';
}
class Bar {
bar = 123;
common = '123'
}
function doStuff(arg: Foo | Bar){
if (arg instanceof Foo){
console.log(arg.foo)
console.log(arg.bar)
console.log(arg.common)
}
if (arg instanceof Bar) {
console.log(arg.foo)
console.log(arg.bar)
console.log(arg.common)
}
}
typescript能够推断出else中的类型
class Foo {
foo = 123;
common = '123';
}
class Bar {
bar = 123;
common = '123'
}
function doStuff(arg: Foo | Bar){
if (arg instanceof Foo){
console.log(arg.foo)
console.log(arg.bar)
console.log(arg.common)
} else {
console.log(arg.bar)
}
}
你可以使用===、、!、!=来区分字面量类型。
type Foo = {
kind: 'foo';
foo: number;
}
type Bar = {
kind: 'bar';
bar: number;
}
function doStuff(arg: Foo|Bar) {
if(arg.kind === 'foo'){
console.log(arg.foo)
} else {
console.log(arg.bar)
}
}
TypeScript可以使用==null和!==null来区分null和undefined
interface Foo {
foo: number;
common: string;
}
interface Bar {
bar: number;
common: string;
}
function isFoo(arg: Foo|Bar): arg is Foo {
return (arg as Foo).foo !== undefined
}
function doStuff(arg: Foo | Bar) {
if(isFoo(arg)){
console.log(arg.foo) // 正确
console.log(arg.bar) // 错误
} else {
console.log(arg.bar) // 正确
console.log(arg.foo) // 错误
}
}
TypeScript不能确定类型保护在回调中一直有效,比如下述代码中变量foo的属性bar可能为为undefined,因此typescript会报错
export var foo:{bar?: {baz: string}}
function doCalback(callback: () => void){
callback()
}
if(foo.bar){
console.log(foo.bar.baz)
doCalback(() => {
console.log(foo.bar.baz)
})
}
解决方式为把推断的安全值存放在本地的局部变量中
export var foo:{bar?: {baz: string}}
function doCalback(callback: () => void){
callback()
}
if(foo.bar){
console.log(foo.bar.baz)
const bar = foo.bar
doCalback(() => {
console.log(bar.baz)
})
}
type Direction = 'North' | 'East' | 'South' | 'West'
function move(distance: number, direction: Direction){
console.log(`${direction}: ${distance}`)
}
move(1, 'North')
move(2, "West")
type oneToFive = 1 | 2 | 3 | 4 | 5
type Bools = true | false
function trueOrFalse(arg: Bools) {
console.log(arg)
}
function logNumber(num: oneToFive){
console.log(num)
}
trueOrFalse(false)
logNumber(1)
// 根据传入的数组生成key-value结构
function strEnum<T extends string>(o: Array<T>): {[K in T]: K} {
return o.reduce((res, key) => {
res[key] = key;
return res;
}, Object.create(null))
}
// 然后可以使用keyof, typeof来生成字符串的联合类型
const Direction = strEnum(['North', 'South', 'East', 'West'])
type Direction = keyof typeof Direction
let sample: Direction
sample = Direction.North
sample = 'North'
sample = 'AnythingElse' // 错误
使用readonly关键字来标记属性可以保证数据不可变
我们也可以指定一个类的属性为readonly,然后在声明或在构造函数中初始化它们
// 下面是一个readonly映射类型,接收一个泛型T,用来把它所有属性都标记为readonly
type Foo = {
bar: number;
bas: number;
}
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
type ReadonlyFoo = MyReadonly<Foo>;
const foo1: Foo = {bar: 123, bas: 456}
const fooReadonly: ReadonlyFoo = {bar: 123, bas: 456}
foo.bar = 456
fooReadonly.bar = 789 // 报错
import React, { PureComponent } from 'react'
import { Layout, Menu } from 'antd'
import { connect } from 'dva'
import { Link } from 'react-router-dom'
import { Urls } from '@/config'
const { Header, Content } = Layout
interface Props {
readonly [index: string]: any
}
interface State {
menus: Array<{
readonly key: string
readonly label: string
readonly to: string
}>
}
class App extends PureComponent<Props, State> {
state = {
menus: [
{
key: 'main',
label: '首页',
to: Urls.MAIN
},
{
key: 'form',
label: '表单设计',
to: Urls.FORM
},
{
key: 'code',
label: '代码生成',
to: Urls.CODE
},
{
key: 'chart',
label: '图表设计',
to: Urls.CHART
}
]
}
renderMenu(): JSX.Element {
const { menus } = this.state
return (
<Menu theme={'dark'} mode={'horizontal'}>
{menus.map((menu) => (
<Menu.Item key={menu.key}>
<Link to={menu.to}>{menu.label}</Link>
</Menu.Item>
))}
</Menu>
)
}
render(): JSX.Element {
return (
<Layout className={'height100'}>
<Header>{this.renderMenu()}</Header>
<Content>{this.props.children}</Content>
</Layout>
)
}
}
export default connect(({ app }) => ({ ...app.toJS() }))(App)
interface Foo {
readonly [x: number]: number;
}
const foo1: Foo = { 0: 123, 2: 345 }
console.log(foo1[0])
foo1[0] = 456 // 报错,只读不可修改
// 以不变的方式使用原生数组,可以使用TypeScript提供的ReadonlyArray接口
let foo2: ReadonlyArray<number> = [1,2,3]
console.log(foo2[0])
foo2.push(4) // 报错,只读数组不可添加元素
foo2 = foo2.concat(4) // 这样是允许的,因为通过concat创建了一个副本
class Animals {
name: string = 'dog';
age: number = 2;
get info() {
return this.name + '' + this.age;
}
}
const animal1 = new Animals()
console.log(animal1.info)
animal1.info = 'cat2' // 报错,只读属性不可重新赋值
readonly和const
readonly用于属性, const用于变量声明
readonly能确保属性不能直接被使用者修改,但是当你把这个属性交给其他并没有做出这种保证的使用者(出于类型兼容的原因而被允许)时,他们可以修改它
const foo1: {
readonly bar: number;
} = {
bar: 123
}
foo1.bar = 456 // 报错,只读属性不能被修改
function iMutateFoo(foo: { bar: number }) {
// 处于类型兼容考虑readonly的属性被修改了
foo.bar = 456
}
iMutateFoo(foo1)
interface Foo {
readonly bar: number
}
function iMutateFoo1(foo: Foo) {
foo.bar = 456 // 因为明确了参数的类型,则此处只读属性bar不可被修改了
}
iMutateFoo1(foo1)
设计泛型的关键动机是在成员之间提供有意义的类型约束,这些成员可以是类的实例成员、类的方法、函数的参数、函数返回值。
class Queue<T> {
private data:T[] = [];
push = (item: T) => this.data.push(item);
pop = (): T | undefined => this.data.shift();
}
const queue = new Queue<number>();
queue.push(1)
queue.push('1') // 报错,指定泛型类型为number类型,不能再添加其他类型的元素了
function reverse <T> (arr: T[]): T[] {
const res: T[] = []
for( let i = arr.length - 1; i >= 0; i-- ){
res.push(arr[i])
}
return res
}
console.log(reverse([1, 2, 3]))
建议:你可以随意调用泛型的参数,当你使用简单的泛型时,泛型常用T、U、V表示。如果在你的参数里,拥有不止一个泛型,你应该使用一个更语义化的名称,如TKey和TValue。依照惯例,以T作为泛型的前缀
3. 一个用于加载JSON文件的返回值函数, 它会返回任何你传入的类型的Promise
const getJSON = <T>(config: {url: string; headers?: {[key: string]: string}}): Promise<T> => {
const fetchConfig = {
method: 'GET',
Accept: 'application/json',
'Content-Type': 'application/json',
...(config.headers || {})
}
return fetch(config.url, fetchConfig).then<T>(response => response.json());
}
type LoadUserResponse = {
user: {
name: string;
emial: string;
}
};
function loadUsers(){
return getJSON<LoadUserResponse>({ url: 'http://example.com/users'});
}
loadUsers().then(res => {
console.log(res.user.name)
})
type Animals = {
name: string;
age: number;
}
type Website = {
url: string;
count: number;
}
function log<T>(arg: T){
console.log(arg)
}
log<Animals>({
name: 'dog',
age: 2
})
log<Website>({
url: 'http://baidu.com',
count: 10000
})
TypeScript可以根据一些简单的规则来推断(然后检查)变量的类型
const a = 123 // 推断为number
let b = 'hello' // 推断为string
b = 123 // 错误
function add(a: number, b: number){
return a + b;
}
// 推断返回类型为number
type Adder = (a: number, b:number) => number
let foo1: Adder = (a, b) => a + b // a,b被推断为number类型
const obj = {
a: 1,
b: '1'
}
obj.a = '2' // 报错
const obj = {
a: 1,
b: '1'
}
let { a } = obj
a = '2' // 报错
标记noImplicitAny用来指示编译器,在无法推断一个变量的类型时,发出一个错误(或者只将其作为一个隐式的any)。此时,你可以做如下处理。
类型兼容性用于确定一个类型能否赋值给其他类型。如string类型与number类型不兼容,因此不能相互赋值。
TypeScript类型系统设计得很方便,它允许你有一些不正确的行为。例如,任何类型都能被赋值给any,这意味着编译器允许你做任何想做的事情。
let foo1: any = 123
foo1 = 'hello'
TypeScript对象是一种结构化的类型,这意味着只要结构匹配,名称也就无关紧要了
interface Point {
x: number;
y: number;
}
class Point2D {
constructor(public x: number, public y: number){}
}
let p: Point;
p = new Point2D(1, 2)
当你比较两个函数类型是否兼容时,下面是一些需要考虑的事情
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
let aoo = (x: number, y: number) => {};
let bar = (x?: number, y?:number) => {};
let bas = (...args: number[]) => {};
aoo = bar
bas = bar
bas = aoo
// 可选的参数(如上面例子中的bar)与不可选的参数(如上面例子中的foo),只有在strictNullChecks为false时兼容
export interface Event {
timestamp: number;
}
export interface MouseEvent extends Event {
x: number;
y: number;
}
export interface KeyEvent extends Event{
keyCode: number;
}
enum EventType {
Mouse,
KeyBoard
}
function addEventListener(eventType: EventType, handler: (n: Event) => void) {}
// 不安全,但是常用
const handler = ((e: MouseEvent) => console.log(e.x+ ',' + e.y))
addEventListener(EventType.Mouse, handler) // 报错
// 解决方案
const handler1 = <(e: Event) => void>((e: MouseEvent) => console.log(e.x+ ',' + e.y))
addEventListener(EventType.Mouse, handler1) // 报错
// 或者
const handler2 = ((e: Event) => console.log((<MouseEvent>e).x+ ',' + (<MouseEvent>e).y))
enum Status {
Ready,
Waiting
}
let status1 = Status.Ready
let num1 = 0
status1 = num
enum Status {
Ready,
Waiting
}
enum Color {
Red,
Blue,
Green
}
let status1 = Status.Ready
let color1 = Color.Blue
status1 = color1 // 报错
只比较实例成员和方法,构造函数和静态成员不起作用。
class Animal1 {
static feet: number;
constructor(name: string, numFeet: number){}
}
class Size {
static feet: number;
constructor(meters: number){}
}
let a: Animal1
let s: Size = new Size(1)
a = s // 可以赋值
s = a // 可以赋值
私有的和受保护的成员必须来自相同的类。
class Animal1 {
protected feet: number | undefined;
}
class Cat extends Animal1{}
let animal1: Animal1;
let cat1: Cat = new Cat();
animal1 = cat1 // 可以赋值
cat1 = animal1 // 可以赋值
class Size {
protected feet: number | undefined;
}
let size: Size;
animal1 = size // 错误, 不能赋值
size = animal1 // 错误,不能赋值
TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
// @ts-ignore
x = y
// 泛型T因为没有被**任何接口的任何成员**使用,因此并不会影响到兼容性
如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:
// @ts-ignore
let identity = function<T> (x: T): T {
// ...
}
// @ts-ignore
let reverse = function<U> (y: U): U {
//...
}
identity = reverse // (x: any) => any 类型与(y: any) => any匹配
当泛型被成员使用时,它将在实例化泛型后影响兼容性
interface Empty<T> {
data: T;
}
let x: Empty<number>;
let y: Empty<string>;
x = y // 错误,不能赋值, 因为泛型T被interface Empty的成员data使用了
never类型是任何类型的子类型,也可以赋值给任何类型, 然而,没有类型是never的子类型或可以赋值给never类型(除了never类型本身之外),即使any也不可以赋值给never类型,通常表现为抛出异常或者无法执行到终止点(例如无限循环)。
let x: never;
let y: number;
// 运行错误,数字类型不能转为 never 类型
x = 123;
// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();
// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();
// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
throw new Error(message);
}
// 返回值为 never 的函数可以是无限循环这种无法被执行到的终止点的情况
function loop(): never {
while (true) {}
}
当类中含有字面量成员时,我们可以用该类的属性来辨析联合类型
如果你使用类型保护风格的检查(即==、=、!=、!),或者使用具有判断性的属性(在这里是kind),TypeScript将会认为你所使用的对象类型必须要具有特定的字面量,并且为你进行类型缩小。
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Circle {
kind: 'circle';
radius: number;
}
type Shape = Square | Rectangle | Circle
function area(s: Shape){
if(s.kind === 'square') {
return s.size * s.size
} else if(s.kind === 'rectangle') {
return s.width * s.height
} else if(s.kind === 'circle') {
return Math.PI * s.radius ** 2;
} else {
const _exhaustiveCheck: never = s
return _exhaustiveCheck
}
}
function areaBySwitch(s: Shape){
switch (s.kind) {
case 'square':
return s.size * s.size;
case 'rectangle':
return s.width * s.height;
case 'circle':
return Math.PI * s.radius ** 2;
default:
const _exhaustiveCheck: never = s
return _exhaustiveCheck
}
}
type DTO = | {
version: undefined;
name: string;
} | {
version: 1;
firstName: string;
lastName: string;
} | {
version: 2;
firstName: string;
lastName: string;
middleName: string;
}
function printDTO(dto: DTO){
if(dto.version == null){
console.log('====================================');
console.log(dto.name);
console.log('====================================');
} else if (dto.version === 1){
console.log(dto.firstName, dto.lastName);
} else if(dto.version === 2){
console.log('====================================');
console.log(dto.firstName, dto.middleName, dto.lastName);
console.log('====================================');
} else {
const _exhaustiveCheck: never = dto;
}
}
import { createStore } from 'redux'
type ActionTypes = 'INCREMENT' | 'DECREMENT'
type Action = {
type: ActionTypes
}
const counter = (state = 0, action: Action) => {
switch(action.type){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
let store = createStore(counter);
store.subscribe(() => {
console.log(store.getState())
})
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
可以用字符串访问JavaScript中的对象(在TypeScript中也一样),并保存对其他对象的引用除字符串外,它也可以保存任意的JavaScript对象,例如一个类的实例
const obj = {
toString(){
console.log('to string is called')
}
}
let foo = {}
foo[obj] = 'hello'
当你向索引签名传入一个其他对象时,JavaScript会在得到结果之前先调用toString方法。
只要索引位置使用了obj,toString方法都将被调用。
const obj = {
toString(){
console.log('to string is called')
}
}
let foo = {}
foo[obj] = 'hello'
console.log(foo[obj])
首先,因为JavaScript在任何一个对象索引签名上都会隐式调用toString方法,而在TypeScript中它将会抛出一个错误提示
我们必须这么做才能解决报错
const obj = {
toString(){
return 'Hello'
}
}
const foo1: any = {}
foo[obj.toString()] = 'World'
const mailBox: {
[index: string]: {
content: string;
};
} = {};
mailBox['a'] = {
content: 'a'
}
mailBox['b'] = {
content: 'b'
}
索引签名的名称,如{[index:string]:{message:string}}里的index,除提高了可读性外,并没有任何意义。例如,如果有一个用户名,你可以使用{username:string}:{message:string},这有助于下一个开发者理解你的代码。
所有成员必须符合字符串索引签名
type keys = 'name' | 'age' | 'job'
type Index = { [k in keys]?: number | string}
const obj: Index = {
name: 'david',
age: 26,
job: 'frontender'
}
interface Obj {
[key: string]: string | number;
[index: number]: string;
}
let obj: Obj = {}
obj['a'] = 3
obj[1] = '1'
interface NestedCSS {
color?: string; // 在strictNullChecks = false时,索引签名可以为undefined
[selector: string]: string | NestedCSS;
}
const example: NestedCSS = {
color: 'red',
'.subclass': {
color: 'blue'
}
}
尽量不要用这种方式把字符串索引签名与有效变量混合使用。否则,如果属性名称中有拼写错误,这个错误将不会被捕获。
相反,我们要把索引签名分离到自己的属性里,如命名为nest,或children、subnodes等。
interface NestedCSS {
color?: string; // 在strictNullChecks = false时,索引签名可以为undefined
nest?: {
[selector: string]: NestedCSS
}
}
const example: NestedCSS = {
color: 'red',
nest: {
'.subclass': {
color: 'blue'
},
'.subclass1': {
color: 'red'
}
}
}
这个时候如果属性名出现拼写错误就能正常进行错误提示
function validate(
value: number
): {
error?: string;
} {
if (value < 0 || value > 100) {
return { error: "Invalid Value" };
}
}
// 定义构造函数类型
type Constructor<T = {}> = new (...args: any[]) => T;
// 添加属性的混合实例
function TimesTamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
}
}
// 添加属性和方法的混合实例
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActivated = false;
activated(){
this.isActivated = true;
}
deactivated(){
this.isActivated = false;
}
}
}
// 简单类
class User {
name = '';
constructor(name?: string){
this.name = name;
}
}
// 通过混合获得有时间戳的用户
const TimestampUser = TimesTamped(User);
const timestampUser = new TimestampUser();
console.log(timestampUser.timestamp);
// 通过混合获取有时间戳并可以激活禁用的用户
const TimestampActivatedUser = TimesTamped(Activatable(User));
const timestampActivatedUser = new TimestampActivatedUser('david');
console.log(timestampActivatedUser.name, timestampActivatedUser.timestamp)
通过ThisType,我们可以在对象字面量中输入this,并提供通过上下文类型控制this类型,它只有在–noImplicitThis的选项下才有效。
type ObjectDescriptor<D, M> = {
data?: D;
methods: M & ThisType<D & M>;
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods;
return { ...data, ...methods } as D & M;
}
let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx;
this.y += dy;
},
},
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
console.log(obj.x, obj.y)