任意字符串的属性名 [propName: string]
interface SquareConfig {
name: string;
[propName: string]: any;
}
let c: SquareConfig = {name: '自定', age: 24, gender: '男'};
type Person = [number, ...string[]];
// 一个数组,第一个元素是number,剩下的元素是不确定长度的string
const person: Person = [1, 'str1', 'str2'];
interface Iprop {
func1: () => string; // 函数返回一个string;
func2: (a: string, b: number) => void; // 函数输入两个参数,没有返回值;
str1: string; // str1参数必须是字符串;
value: Array<{ type: string; money: string; }>;
value: { type: string; money: string; }[];
// 数组,每个元素都是一个对象,对象中有两个元素:
// 一个是type: string类型,一个是money: string类型;
onChange: (data: Array<{ type: string; money: string; }>) => void;
// 函数:输入的是数组,没有返回值
unit?: any[]; // 可有可无,如果有,则应该是数组;
}
function greeter(props: Iprop) {
return ...;
}
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
type Iprop = {
name: string,
age: number
}
const obj: Iprop = {
name: 'Jayson',
age: 23
}
function of<Type>(a: Type) : Type[] {
return [a];
}
const toNumbers = of(1); // const toNumbers: number[]
const toStrings = of("Test Of"); // const toString: string[]
const toNumbers = of<number>(1); // const toNumbers: number[]
const toStrings = of<string>("Test Of"); // const toString: string[]
type Condition = {[key:string]: string};
interface ConditionTypes {
conditionType?: string | any;
conditions?: Condition | Condition[];
[propName: string]: any;
}
interface Props {
conditionTypes: ConditionTypes[];
onSubmit: (data: any) => void;
screenHistory: any;
Layout: any;
}
TS的出现是为了解决JS存在的问题。那么JS有哪些问题呢?
let a = 10;
// 之后a可以随意变化类型
目前,变量a属于
number
类型,它想要在其他地方使用,参与运算;
如果一不小心,开发人员将a
改成了string
类型或者Array
类型,那就会出现bug了,但是JS是不报错的(非常头疼)。
JS起源: JS产生于第一次浏览器大战,大战双方是网景公司和微软公司,网景公司为了和微软公司去拼,设计了JS,时间比较仓促,所以有些设计考虑的不是很周到,比如说IE浏览器的兼容问题;
JS目前在前端属于一个无可代替的语言,它的灵活性和易读性都不可替代;微软公司为了解决JS 动态类型的问题,另辟蹊径,提出了TS的概念;(编辑器VSCode 和 TS 均由微软设计)
TS完全支持JS,可以在任何支持JS的平台中执行;并在JS的基础上增加了一些功能:给变量赋予类型,让JS从一个动态类型的语言变成了静态类型的语言;
但是注意: TS不能被JS解析器直接执行;比如:一个 .ts 文件不能直接在浏览器中执行,需要做一步转化,先将 TS 编译为 JS;(这时候需要安装一个TS编译器)
那么TypeScript增加了什么?
- 强大的开发工具(一些提示)
- 丰富的配置选项 (可以将文件转化为任意版本的JS,比如:ES6、ES5等等)
- 添加ES不具备的新特性
首先需要装一个TS解析器;装TS解析器需要先下载Node.js
- 下载并安装node.js
- 使用npm安装TypeScript
npm i -g typescript
- 创建一个ts文件
- 在ts文件目录下使用tsc对ts文件进行编译,编译好之后,会生成一个.js文件
tsc helloTS.ts
JS:
let a;
a = 10;
a = 'hello';
TS:(TS很友好,发生类型错误时,仍可以编译,但会报错提示开发人员type Error)
// 声明变量,并指定变量类型
let a: number; // a的类型设置为了number,以后的使用过程中,a的值只能是数字
a = 10; // no problem
a = 33; // no problem
a = 'hello'; // 编译器,会标红;
// 并提示 Type 'hello' is not assignable to type 'number'.
// 声明完变量直接进行赋值
let b: boolean = true;
b = 123; // 报错
// 如果变量的声明和赋值同时进行的;TS可以自动对变量进行类型检测
let c = true;
c = 123; // 报错
JS中的函数是不考虑参数的类型和个数的;
JS:
function sum(a, b) {
return a + b;
}
console.log(sum(123, 456); // 579
console.log(sum(123, '456'); // '123456'
TS:
// 函数只有两个参数,参数类型都是number,函数返回值也是number类型
function sum(a: number, b: number): number {
return a + b;
// return a + 'hello'; // 会报错
}
let result = sum(123, 456);
let b: boolean | string; // 变量b的类型只能是boolean或者string;
let c: {name: string} & {age: number};
c = {name: '孙悟空', age: '18'};
let contract = 'Actor'; //变量contract只能是'Actor'这个string值;
// 字面量类型中的联合类型
let a: 'male' | 'female'; // 变量a的值只能是'male'或者'female',类型是string
let c: any;
// 一个类型设置为any,相当于对该变量关闭了TS的类型检测
c = 10;
c = 'hello';
使用 any
进行类型限制,相当于移除了类型检查;我们不推荐使用 any
,但它有存在的必要性:比如:
let notSure: any = 4;
notSure.ifItExists();
// okay, ifItExists might exist at runtime
notSure.toFixed();
// okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed();
// Error: Property 'toFixed' doesn't exist on type 'Object'.
由于在TypeScript语言中,一切皆对象,因此,大部分人会有疑问? Object
会和 any
有相似的作用吗?直接挑明,有;但是,Object类型的变量只允许给它赋任意值,但不能够调用变量上的方法。
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
// unknown 表示未知类型的值
let d: unknown;
d = 10;
d = 'hello';
乍一看,unknown和any类型限制是一样的,实则不然,注意往下看:
let e: boolean;
// d 的类型是any,它可以赋值给任意类型
e = c; // any类型祸害了变量e, 现在变量e的值为:'hello'
unknown 类型的变量不能直接赋值给其他变量;
let s: string;
s = d; // 虽然d现在是string类型,但是会标红;
// Type 'unknown' is not assignable to type 'string';
// 因为现在把未知类型的变量赋值给了已知类型的变量s
解决办法:判断类型后再做赋值; // 类型断言
if(typeof e === 'string) {
s = e;
}
// 类型断言: 告诉解析器,变量的实际类型
s = e as string;
s = <string>e;
function fn(n: number): string | number {
if(n > 1) {
return '大于1';
} else {
return n;
}
}
当函数没有返回值的话,使用void;返回null或者返回undefined,都认为是没有返回值。
function fn(n: number): void {
if(n >= 0 && n < 1) {
return null;
} else if(n < 0) {
return undefined;
} else {
console.log(n);
}
}
function fn(n: number): never {
if(n >= 0 && n < 1) {
throw new Error('报错了');
} else {
console.log('n 出问题了');
throw new Error('n 有问题');
}
}
let a: object; // 此时a可以是对象,也可以是函数,因为函数也属于对象
// {} 用来指定对象中可以包含哪些属性
// 语法: {属性名: 属性值的类型, 属性名: 属性值的类型};
// 这里注意:属性名后面加一个?,表示属性是可选的
let b: {name: string, age?: number};
b = {name: 'Jayson柴'};
// 任意字符串的属性名 [propName: string]
let c: {name: string, [propName: string]: any};
c = {name: '自定', age: 24, gender: '男'};
let a: Function;
// (形参: 类型, 形参: 类型 ...) => 返回值的类型
let b: (a: number, b: number) => number;
b = function(n1: number, n2: number): number {
return 10;
};
// string[] 表示字符串数组
let c: string[];
c = ['a', 'b', 'c'];
// Array 表示字符串数组
let d: Array<string>;
d = ['a', 'b', 'c'];
tuple
元祖类型// 元祖就是固定长度的数组
let a: [string, number?]; // 长度为2的元祖
a = ['hello', 123];
a = ['hello'];
enum
枚举类型(TS新增类型)enum Gender {
Male = 1,
Female = 0,
};
let b: {name: string, gender: Gender};
b = {
name: '柴勇',
gender: Gender.Male,
};
默认从0开始为元素编号;
enum Color {Red, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // Blue
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // Green
类型断言有两种形式:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
⚠️ 当在TypeScript中使用 JSX 时,只有 as
语法断言是被允许 ✅ 的。
// 类型别名
type MyType = 1 | 2 | 3 | 4 | 5;
let k: myType;
k = 2;
// 描述一个对象的类型
type Iprop = {
name: string,
age: number
}
const obj: Iprop = {
name: 'Jayson',
age: 23
}
之前对.ts文件的编译需要输入tsc xx.ts
;每当文件内容发生变化,都需要重新输入tsc xx.ts
;为了解决这个问题,第一次可以这样输入:
tsc xx.ts -w // -w 表示watch监视,ts文件改变后,会自动监视到文件变化,重新编译;
但是这样会有一个问题:有n个文件,就需要输入n次,比较繁琐;
解决办法:在项目文件夹中添加一个tsconfig.json文件,进行相关配置;
{
/*
tsconfig.json是ts编译器的配置文件,
ts编译器可以根据它的信息来对代码进行编译
"include" 用来指定哪些ts文件需要被编译
路径: ** 表示任意目录
* 表示任意文件
"exclude": 不需要被编译的文件目录
默认值:["node_modules", "bower_components", "jspm_packages"]
"extends": 继承配置文件
"files": 指定被编译文件的列表,只有需要编译的文件少时才会用到;和include很像
*/
"include": [
"./src/**/*",
"./page/**/*"
],
"exclude": [
"./src/hello/**/*"
],
"extends": "./config/base",
"files": [
"core.ts",
"sys.ts",
],
/*
compilerOptions 编译器的选项
*/
"compilerOptions": {
"target": "ES6", // 指定ts被编译的ES版本,默认编译为ES3
"module": "amd", // 指定要使用的模块化的规范,可以选择commonjs, amd, umd等等;
// es6: import { Hello } from './Hello';
// commonjs:
// const Hello_js_1 = require("Hello.js");
// console.log(Hello_js_1.Hello)
"lib": ["DOM", "ES2015"], // lib 用来指定项目中要使用的库
"outDir": "./dist", // outDir 用来指定编译后文件所在的目录
"outFile": "./dist/app.js", // 将代码合并为一个文件
// 将所有全局作用域的代码编译好之后合并到app.js文件中
// 使用了模块化,则无法合并
"allowJs": false, // 是否对js进行编译,默认是false,不编译js文件
"checkJs": false, // 检查js代码是否符合语法规范。默认是false,不检查
"removeComments": false, // 编译结果中是否移除注释,默认是false,不移除注释
"noEmit": false, // 不生成编译后的文件,默认是false,编译后生成文件
// 改为true后,执行了编译过程,但是不生成编译文件
"noEmitOnError": false,
// 当有错误时,编译后不生成编译后的文件;默认false,不开启功能
"alwaysStrict": true, // 用来设置编译后的文件是否使用严格模式,默认false,不设置严格模式
// true:编译后的js文件开头:'use strict'
// 当代码中有引入 / 导出模块时,默认启用严格模式,不需要在开头声明'use strict'
"noImplicitAny": false, // 当变量没有设置类型时,默认是any,编译器不会标红
// true:不允许隐式any,浏览器会标红
"noImplicitThis": false, // 是否允许this的类型不明确,默认false,允许
// true:不允许不明确类型的this,一定要知道this的类型是什么;
"strictNullChecks": false, // 是否严格检查空值;默认false,不严格
// 下面例子,如果box1没有获取到节点,则box1为一个null
/*
let box1 = doucment.getElementById('box1');
box1.addEventListener('click', function() {
alert('hello');
});
*/
// 解决办法:
/*
box1?.addEventListener('click', function() {
alert('hello');
});
if(box1 !== null) {
box1.addEventListener('click', function() {
alert('hello');
});
}
*/
"strict": false, // 所有严格检查的总开关
// 如果设为true, 则所有的严格检查都可以不写,比如noImplicitAny等等;
}
面向对象是程序中一个非常重要的思想;程序之中所有的操作都是通过对象完成的。
举例:
- 操作浏览器要使用window对象;
- 操作网页要使用document对象;
- 操作控制台要使用console对象;
- 类中的成员默认被指定为:
public
, 指定为public
的成员可以在任何地方使用;- 一旦被指定为
private
,则该成员只能在类内部使用,不能在类的实例中使用,也不能在类的子类中使用;- 一旦被指定为
protected
,则该成员可以在类或者子类内部使用,不能在类或者子类的实例中使用。
创建对象,必须先定义类(可以理解为对象的模型),程序中可以根据类创建指定类型的对象;
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
// 构造函数会在对象创建时调用
// 这里的this表示当前的实例对象
this.属性名 = 参数;
this.name = name;
this.age = age;
}
// 实例方法
sayHello() {
console.log('Hello,' + this.name);
};
// 类方法 / 静态方法,可以通过类去调用,不能通过实例去调用
static sayHi() {
console.log('Hi);
};
}
const per = new Person('霄强', 29);
per.sayHello(); // 调用实例方法 Hello, 霄强
Person.sayHi(); // 调用类方法
class Men extends People {
gender: string;
constructor(name: string, age: number, gender: string) {
// 如果在子类中写了构造函数,则在子类构造函数中必须通过super调用父类的构造函数
// 如果子类不写构造函数(constructor),则默认继承父类所有属性和方法
super(name, age); // 调用父类的构造函数
this.gender = gender;
}
sayHello() {
// 在类的方法中,super表示当前类的父类
super.sayHello();
}
}
抽象类中可以设置抽象方法,子类必须对抽象方法进行重写;
抽象方法的目的: 防止子类忘记写某个方法;(不重写方法,会标红)
// 以abstract开头的类是抽象类
// 抽象类和其他类区别不大,只是不能用来创建对象
// 抽象类是专门用来被继承的类
// 抽象类中可以添加抽象方法
abstract class Person {
constructor() {}
// 定义一个抽象方法
// 抽象方法使用abstract开头,没有方法体
// 抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写
abstract sayHello(): void;
}
// 描述一个对象的类型
type Iprop = {
name: string,
age: number
}
const obj: Iprop = {
name: 'Jayson',
age: 23
}
接口 interface
用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法
同时,接口也可以当作类型声明去使用,此时的用法和 type
一样。
⚠️ 注意
使用 interface
当作类型声明,命名可以一样,应用该类型声明的变量结合多个类型声明使用;
但是type 当作类型声明,命名不能重复;
interface myInterface {
name: string;
age: number;
}
interface myInterface {
gender: string;
}
const obj: myInterface = { name: 'Jayson', age: 23, gender: 'male' };
一个接口可以继承多个接口,创建出多个接口的合成接口;
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
接口可以在定义类的时候去限制类的结构 (和抽象类很相似),但是 接口中所有的属性不能有实际的值;
接口只定义对象的结构,而不考虑实际值;在接口中所有的方法都是抽象方法;
interface myInter {
name: string;
sayHello(): void;
}
// 定义类时,可以使类去实现一个接口,实现接口就是使类满足接口的要求
class MyClass implements myInter {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
console.log('Hello');
}
}
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
};
}
const per = new Person('霄强', 29);
console.log(per); // Person {name: '霄强'. age: 29}
per.name = '柴';
per.age = -29;
console.log(per); // Person {name: '柴'. age: -29}
上面的写法有一个很大的问题,属性在对象中设置,属性可以被任意的修改,可能会导致对象中的数据变得非常不安全。(比如不小心将age设置为负,或者将money设置为负,可能会出问题)
TS中可以在属性前添加属性的修饰符
- public 修饰的属性可以在任意位置访问(修改),属性默认是public;
- private 私有属性,只能在类内部进行访问(无法修改);
- protected 受保护的属性,只能在当前类和当前类的子类中访问;
⚠️ constructor
也可以使用修饰符 protected
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.
⚠️ 但是我们可以通过在类中 添加方法 使得私有属性可以被外部访问
class Person {
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
};
getName() {
return this._name;
};
setName(value: string) {
// 条件设置,防止出错
if(value >= 0) {
this._name = value;
}
}
}
const per = new Person('霄强', 29);
console.log(per); // Person {_name: '霄强'. _age: 29}
console.log(per.getName()); // '霄强'
per.setName('柴');
console.log(per.getName()); // '柴'
console.log(per._name); // 标红,无法访问
per._name = '柴'; // 标红,无法更改属性值
上面的操作可能略显繁琐,我们可以设置 get 和 set 方法
class Person {
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
};
get name() {
return this._name;
};
set name(value: string) {
this._name = value;
};
}
const per = new Person('霄强', 29);
console.log(per.name); // 29
per.name = '柴'; // 可以直接修改name
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
}
}
// 简写: 语法糖
class Person {
constructor( public name: string, public age: number) {}
}
在定义函数或者类时,遇到类型不明确就可以使用泛型。
// 函数的返回值和函数参数的类型是一致的;
// 不要使用any,使用any就相当于关闭了类型检查;
function fn<T>(a: T): T {
return a;
}
// 调用具有泛型的函数
let result = fn(10); // 不指定泛型,TS可以自动推断类型 T 为 number;
let result1 = fn<string>('hello'); // 指定泛型;
泛型可以指定多个
function fn2<T, K>(a: T, b: K): T {
return a;
}
fn2<number, string>(123, 'hello');
对泛型进行限制
interface Inter {
length: number;
}
function fn3<T extends Inter>(a: T): number {
return a.length;
}
fn3('hello') // 这里fn3的参数只能是有length的类型
class MyClass<T> {
name: T;
constructor(name: T) {
this.name = name;
}
}
const myclass = new MyClass<string>('柴勇');
keyof
的应用keyof
操作符提取属性名称,常用场景:对象中,获取键对应的值时(只有正确的key才能拿到值,不正确的键会报错);
interface StringIndexArray {
[index: string]: string;
}
interface NumberIndexArray {
[index: number]: string;
}
type K1 = keyof StringIndexArray // type K1 = string | number
type K2 = keyof NumberIndexArray // type K2 = number
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number
这里要注意:
function prop(obj: object, key: string) {
return obj[key];
}
这样使用的话,编辑器会提示错误:
// Element implicitly has an 'any' type because expression of type 'string'
// can't be used to index type '{}'.
元素隐式地拥有 `any` 类型, `string` 类型不能用于索引`{}`类型;
暴力解决方案:
function prop(obj: object, key: string) {
return (obj as any)[key];
}
更好的解决方案:
定义T类型并使用 extends
关键字约束类型必须是object
类型的子类型 (说白了,传入的obj是个对象就行),然后使用 keyof
操作符获取T类型的所有键,返回类型是联合类型;
下面的案例中,T
类型是object
类型,K
类型继承T类型
的所有键;所以K
的值必须是id、text、done
中的一个;
这样就防止开发人员,读取不存在的类型;比如:prop(todo, "date");
是会报错的。
type Todo = {
id: number;
text: string;
done: boolean;
}
const todo: Todo = {
id: 1,
text: "Learn TypeScript keyof",
done: false
}
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean
typeof
操作符用于获取变量的类型
const COLORS = {
red: 'red',
blue: 'blue'
}
// 首先通过typeof操作符获取color变量的类型,然后通过keyof操作符获取该类型的所有键,
// 即字符串字面量联合类型 'red' | 'blue'
type Colors = keyof typeof COLORS
let color: Colors;
const COLORS = {
red: 'red',
blue: 'blue'
}
// 首先通过typeof操作符获取color变量的类型,然后通过keyof操作符获取该类型的所有键,
// 即字符串字面量联合类型 'red' | 'blue'
type Colors = keyof typeof COLORS
let color: Colors;
color = 'red' // Ok
color = 'blue' // Ok
// Type '"yellow"' is not assignable to type '"red" | "blue"'.
color = 'yellow' // Error
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript具有ReadonlyArray
类型,它与Array
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
想要再次被修改,只能做类型断言
a = ro as number[];
那么,问题来了?
const
也可以阻止变量修改,那么 const
和 readonly
有什么区别呢?
什么时候该用 const
, 什么时候该用 readonly
?
看要把它作为变量使用还是作为一个属性:
- 作为变量使用的话,用
const
;- 若做为属性,则使用
readonly
;