数据类型
基础类型
- string
- number
- boolean
- array number[]
- 元组 [number,string]
- undefined
- null
- any
- unknown
- void 用在没返回的函数上
- never (抛出异常 & 无限循环代码)
层级关系
- Top Type: any & unknown
- 特殊的 Object ,它也包含了所有的类型,但和 Top Type 比还是差了一层
- String、Boolean、Number 这些装箱类型
- 原始类型与对象类型
- 字面量类型,即更精确的原始类型与对象类型嘛,需要注意的是 null 和 undefined 并不是字面量类型的子类型
- never 类型
unknown
unknown 是 TypeScript 3.0 引入的新类型,它的作用是表示未知类型的值。任何类型的值都可以赋值给 unknown 类型,但是 unknown 类型的值不能直接赋值给其他类型,只能赋值给 unknown 类型或者 any 类型。
let value: unknown
value = true
value = 1
value = 'hello'
value = []
value = {}
value = Math.random
value = null
value = undefined
const val1: string = value // Error
const val2: number = value // Error
const val3: () => {} = value // Error
const val4: {} = value // Error
const val5: any = value
const val6: unknown = value
要对 unknown 类型进行属性访问,需要进行类型断言。
let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo()
never
never 表示永远不会返回的类型(什么都没有),它可以是抛出异常的函数,也可以是无限循环的函数。
never 类型不携带任何的类型信息,因此会在联合类型中被直接移除。
type UnionWithNever = 'foo' | 599 | true | void | never;
// 这里显示的类型是"foo" | 599 | true | void
never 是整个类型系统里最底层的类型。它是所有类的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型的变量。
通常,never 用于抛出异常或者无法执行到的终点。
function error (message: string): never {
throw new Error(message)
}
也可以用作于类型检查:
比如,一开始有一个类型为 string | number
,需要对不同类型进行不同处理:
declare const strOrNum: string | number
if (typeof strOrNum === 'string') console.log('str!')
else if (typeof strOrNum === 'number') console.log('num!')
else throw new Error(`Unknown input type: ${strOrNum}`)
后来,这个类型变成了 string | number | boolean
,但是我们忘记了修改这里的代码,这时候就会报错。这时候,可以用 never 来帮助类型检查:
if (typeof strOrNum === 'string'){
console.log('str!')
} else if (typeof strOrNum === 'number'){
console.log('num!')
} else {
const _exhaustiveCheck: never = strOrNum
throw new Error(`Unknown input type: ${_exhaustiveCheck}`)
}
因为,由于 TypeScript 强大的类型分析能力,每经过一个 if 语句处理,strOrNum 的类型分支就会减少一个(因为已经被对应的 typeof 处理过)。而在最后的 else 代码块中,它的类型只剩下了 never 类型。
数组
数组声明
const arr1: number[] = [1, 2, 3]
const arr2: Array<number> = [1, 2, 3]
以上两种声明方式是等价的,但是推荐使用第一种。
元组
元组是固定长度的数组,每个元素的类型可以不同。
const tuple: [string, number] = ['hello', 1]
在越界访问时,会报错。
const tuple: [string, number] = ['hello', 1]
tuple[2] // error
元组可以提升数组结构的严谨性,避免出现越界访问。
具名元组
TypeScript 4.0 中,可以给元组中的每个元素定义一个名字。
const tuple: [name: string, age: number] = ['hello', 1]
tuple[0] // 'hello'
对象
- object 只能赋值{}
- Object & {} (代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object)
let bigObject: Object;
object = 1; // 编译正确
object = "a"; // 编译正确
object = true; // 编译正确
object = null; // 报错
ObjectCase = undefined; // 报错
ObjectCase = {}; // ok
interface 接口
通过 interface 关键字定义接口,用来描述对象类型。
interface Person {
name: string;
age: number;
[prop: string]: any; // 其他的任意属性
}
可选
通过在属性名后面加上?
来表示可选属性。
interface Person {
name: string;
age?: number;
}
只读
通过在属性名前面加上readonly
来表示只读属性。
interface Person {
readonly idNum: number;
name: string;
age?: number;
}
继承 extends
接口可以继承多个接口,用,
分隔。
interface Person {
name: string;
age?: number;
}
interface Girl {
shoes: string;
}
interface Student extends Person, Girl {
school: string;
}
enum 枚举
枚举可以拥有更好的类型提示,并且这些常量会被约束在一个命名空间下。
enum API_URL {
ADD_ADMIN = "XXXXX",
EDIT_ADMIN = "XXXX",
DELETE_ADMIN = "XXXX",
}
const xx = API_URL.ADD_ADMIN;
如果没有声明初始值,那么默认值为 0,后续的值会自动加 1。
enum Color {
RED,
BLUE,
GREEN,
}
const myBookColor = Color.BLUE; // 1
使用计算值
只有数值枚举可以使用计算值。
const returnUrl = (type: number) => type * 2;
enum API_URL {
ADD_ADMIN = returnUrl(0),
EDIT_ADMIN = returnUrl(3),
DELETE_ADMIN = returnUrl(4),
}
const xx = API_URL.ADD_ADMIN; // 0
双向映射
可以从枚举值到枚举名字,也可以从枚举名字到枚举值。
仅有值为数字的枚举成员才能够进行这样的双向枚举
enum Color {
RED,
BLUE,
GREEN,
}
const myBookColor1 = Color.BLUE; // 1
const myBookColor2 = Color[2]; // 1
常量枚举
带 const 在编辑阶段删除,只能通过枚举成员访问枚举值。
const enum Color {
RED = "red",
BLUE = "blue",
GREEN = "green",
}
const myBookColor: Color = Color.BLUE;
函数
函数声明
function add(x: number, y?: number): number {
return y ? x + y : x;
}
箭头函数
// 方式一
const foo = (name: string): number => {
return name.length;
};
// 方式二
const foo: (name: string) => number = name => {
return name.length;
};
方式二的可读性较差,推荐以方式一。或者用类型别名将函数声明抽离出来。
type Foo = (name: string) => number;
const foo: Foo = name => {
return name.length;
};
void 和 underfined
void
用在没返回的函数上underfined
用在有返回的但没有返回值的函数上
function foo(): void {
console.log("foo");
}
function bar(): undefined {
console.log("bar");
return;
}
可选参数 和 rest 参数
function foo(name: string, age?: number, ...rest: string[]): void {
console.log(name, age, rest);
}
foo("hello", 1, "a", "b", "c");
可选参数必须位于必选参数之后。带默认值的参数也可以看做是可选参数。
function foo(name: string, age: number = 18): void {
console.log(name, age);
}
foo("hello");
函数重载 Overload Signature
在逻辑比较复杂的情况下,函数可能会有多组入参类型和返回值类型。这时候就可以使用函数重载。
// 重载1 flag为true时返回string
function func(foo: number, flag: true): string;
// 重载2 flag为false时返回number
function func(foo: number, flag?: false): number;
// 函数的实现签名
function func(foo: number, flag?: boolean): string | number {
if (flag) return String(foo);
return foo * 599;
}
Generator 函数
function* foo(): Generator<void> {}
function* bar(): Generator<number> {
yield 1;
yield 2;
yield 3;
}
const a = bar();
console.log(a.next()); // { value: 1, done: false }
console.log(a.next()); // { value: 2, done: false }
console.log(a.next()); // { value: 3, done: false }
console.log(a.next()); // { value: undefined, done: true }
async 函数
async
函数返回的必是一个Promise
对象。
async function foo(): Promise<void> {}
async function bar(): Promise<number> {
return 1;
}
类
Class 的主要结构只有构造函数、属性、方法、访问符(Accessor)。
修饰符
在 typescript 中,修饰符有三种:public
、private
、protected
、readonly
。
public
:默认修饰符,可以在类、类的实例、子类中访问。private
:只能在类中内部访问。protected
:只能在类和子类中访问。只能被继承而不能被实例化,通常用于定义抽象基类。
静态成员
使用static
关键字来标志一个静态成员。静态成员只能通过类名访问,不能通过实例访问。
class Person {
static names = "hello";
static getName(): string {
return Person.names;
}
}
class Student extends Person {
age: number = 18;
}
const y = new Student();
console.log(y.age); // 18
console.log(Person.getName()); // hello
console.log(Student.getName()); // hello
在静态方法中,this 指向的是类本身,所以只能访问同为静态的成员。
静态成员直接挂载上函数体上,而实例成员挂载在原型上(ES5 及以下,ES6 原生支持 static)。
静态成员不会被实例继承,但是可以被子类继承。
可以用类+静态成员来实现命名空间的功能。
class Person {
static name: string = "hello";
static getName() {
return Person.name;
}
}
class Student extends Person {
age: number = 18;
}
console.log(Person.name); // hello
console.log(Person.getName()); // hello
console.log(Student.name); // hello
console.log(Student.getName()); // hello
override 覆盖方法
用override
来确保子类覆盖的方法在父类中存在。
class Base {
foo: string = "foo";
print() {
console.log(Base.name);
}
}
class Derived extends Base {
override print() {
console.log(Derived.name);
}
// error :This member cannot have an 'override' modifier because it is not declared in the base class 'Base'
override xxx() {}
}
抽象类
使用abstract
关键字来定义抽象类,抽象类不能被实例化,只能被继承。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earch...");
}
}
class Cat implements Animal {
makeSound() {
console.log("喵喵喵");
}
move() {
console.log("move move");
}
}
const mm = new Cat();
mm.move();
在 ts 中,无法声明静态的抽象成员。
<> as 类型断言
let str: any = "to be or not to be";
let strLength: number = (<string>str).length;
let str: any = "to be or not to be";
let strLength: number = (str as string).length;
推荐使用as
来获取更好的阅读体验。
双重断言
原类型与断言类型之间差异过大(指鹿为马)时,ts 会报错,这个时候需要双重断言。
先断言到unknown
类型,再断言到目标类型。
const str: string = "xxxx";
(str as unknown as { handler: () => {} }).handler();
! 非空断言
使用!
形式标记前面的声明一定是非空的。
let user: string | null | undefined;
console.log(user!.toUpperCase()); // 编译正确
console.log(user.toUpperCase()); // 错误
| 联合类型
let status: string | number;
status = "to be or not to be";
status = 1;
& 交叉类型
表示两个类型都必须存在
code
interface IpersonA {
name: string
age: number
}
interface IpersonB {
name: string
gender: string
}
const person: IpersonA & IpersonB = {
name: '师爷',
age: 18,
gender: '男'
}
type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (string | number | symbol) & string; // string
如果交叉类型中相同的 key,但是属性修饰符不同时
name
&name?
=name
code
interface A {
name: string
}
interface B {
name?: string
}
type AB = A & B;
type BA = B & A;
const b: B = {}
const ab: AB = {
name: 'xxx'
}
const ba: BA = {
name: 'xxx'
}
:::
name
&readonly name
=name
?name
&readonly name
=name
code
interface A {
name?: string
}
interface B {
readonly name: string
}
type AB = A & B;
type BA = B & A;
const ab: AB = {
name: 'xxx'
}
const ba: BA = {
name: 'xxx'
}
ab.name = '1'
ba.name = '1'
交叉类型中的 key 名称相同但是类型不同,则该 key 为 never 类型
type 类型别名
类型别名用来给一个类型起个新名字。它只是起了一个新名字,并没有创建新类型。
它的主要作用是对一组类型或一个特定结构进行封装,方便重用。
抽离一组联合类型
type StatusCode = 200 | 301 | 400 | 500 | 502;
type MaybeNull<T> = T | null;
抽离一个函数类型
type handlerMouseEvent = (e: MouseEvent) => void;
const handleClick: handlerMouseEvent = (e) => {}
它可以接收泛型,变为更灵活的创建工具
type MaybeArray<T> = T | T[];
function ensureArray<T> (input: MaybeArray<T>): T[] {
return Array.isArray(input) ? input : [input]
}
- type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。
- 可以拓展
- 可以声明基本数据类型别名/联合类型/元组等
[]: 索引签名类型
索引签名类型主要用在 interface 和 type 中,用来快速声明一个键值类型一致的类型结构。
interface Base {
[key: string]: string
}
但是在 js 中,obj[prop]
形式的访问会将数字索引转换为字符串索引访问,所以在字符串索引签名类型中,仍然可以声明数字和 symbol 类型的键名。
interface Base {
[key: string]: string
}
const obj: Base = {
xx: 'xxx',
111: 'foo',
[Symbol('a')]: 'xxx'
}
keyof 索引类型查询操作符
keyof
可以将对象中的所有键转为对应字面量的联合类型。
注意,这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量。
interface Base {
age: number
username: string
}
type BaseKeys = keyof Base;
const key: BaseKeys = 'age'
索引类型访问
interface Base {
age: number
username: string
}
type Age = Base['age']; // number
也可以和 keyof
结合使用
interface Base {
age: number
username: string
}
type BaseValues = Base[keyof Base]; // string | number
泛型
在定义函数、接口、类的时候,先不指定具体的类型。而在使用的时候再指定一种类型。
function getValue<T>(x: T): T {
return x;
}
getValue<string>("yomuki"); // 手动定义
getValue("yomuki"); // 自动推断
泛型约束
用 extend 约束这个泛型必须存在某些类型
interface Persom {
readonly idNum: number; // 只读
age?: number; // 可选
name: string;
[prop: string]: any; // 其他的任意属性
}
function getValue<T extends Persom>(x: T): T {
return x;
}
getValue({
idNum: 123,
name: "yOMUKI",
});
泛型接口
interface Jk<T, U> {
name: T;
age: U;
car: string[];
}
const jk1: Jk<string, number> = {
name: "name",
age: 123,
car: [],
};
内置方法中的泛型
Promise
interface IBase {
name: string
}
const p = new Promise<IBase>((resolve) => {
resolve({
name: 'xxx'
})
})
p.then((res) => {
console.log(res.name)
})
Array.prototype.reduce
const arr: number[] = [1, 2, 3, 4]
const res = arr.reduce<number[]>((prev, cur) => {
prev.push(cur)
return prev
}, [])
in 映射类型
映射类型的主要作用是基于键名映射到键值类型。
type Stringify<T> = {
[K in keyof T]: string;
};
假设传入的泛类是对象,使用keyof
获取对象的键名联合类型,然后通过映射类型in
将这个联合类型的每一个成员映射出来,并将其键值类型设为string
。
type Stringify<T> = {
[K in keyof T]: string;
};
interface Person {
name: string
age: number
isMarried: boolean
}
const a: Stringify<Person> = {
name: 'a',
age: '12',
isMarried: '否'
}
也可以拿到每个键值的类型
type Clone<T> = {
[K in keyof T]: T[K];
};
typeof 类型查询操作符
typeof 返回的是一个 TypeScript 类型。
is 类型守卫
通过类型守卫函数来解决控制流分析的不足。使用关键字is
function isString (input: unknown): input is string {
return typeof input === 'string'
}
function foo (val: number | string) {
if (isString(val)) val.concat('xxx')
}
也可以在类型守卫中使用对象类型、联合类型
export type Falsy = false | '' | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val
// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;
export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean', 'undefined'].includes(typeof val)
asserts 类型断言守卫
和类型守卫最大的不同点在于,在判断条件不通过时,断言守卫需要抛出一个错误,类型守卫只需要剔除掉预期的类型。
function assert (condition: any, msg?: string): asserts condition {
if (!condition) throw new Error(msg)
}
const foo: any = 'yomuki'
assert(typeof foo === 'number')
foo.toFixed()
可以和 is 相结合
function assertIsNumber (val: any): asserts val is number {
if (!(typeof val === 'number')) throw new Error('Not a number!')
}
const foo: any = 'yomuki'
assertIsNumber(foo)
foo.toFixed()
模板字符串类型
类型中也可以使用模板字符串,但是只能使用string
、number
、boolean
、null
、undefined
、bigint
这几种类型。
type Hello<T extends string | number | boolean | null | undefined | bigint> = `Hello ${T}`;
type Hello1 = Hello<'World'>; // "Hello World"
type Hello2 = Hello<null>; // "Hello null"
type Hello3 = Hello<undefined>; // "Hello undefined"
增强字符串字面量类型的灵活性,进一步增强类型和逻辑代码的关联:
type Version = `${number}.${number}.${number}`;
const a: Version = '1.1.1'
const b: Version = 'a.b' // Type '"a.b"' is not assignable to type '`${number}.${number}.${number}`'.
当存在大量有关联的字面量时,可以使用模板字符串工具类型来提高代码的可读性:
// type SKU =
// | 'iphone-16G-official'
// | 'xiaomi-16G-official'
// | 'honor-16G-official'
// | 'iphone-16G-second-hand'
// | 'xiaomi-16G-second-hand'
// | 'honor-16G-second-hand'
// | 'iphone-64G-official'
// | 'xiaomi-64G-official'
// | 'honor-64G-official'
// | 'iphone-64G-second-hand'
// | 'xiaomi-64G-second-hand'
// | 'honor-64G-second-hand';
type Brand = 'iphone' | 'xiaomi' | 'honor';
type Memory = '16G' | '64G';
type ItemType = 'official' | 'second-hand';
type SKU = `${Brand}-${Memory}-${ItemType}`;
// 可以利用差集来剔除不需要的字面量
与索引类型 和 映射类型 结合使用
interface Foo {
name: string
age: number
}
interface ChangeListener {
on: (change: `${keyof Foo}Changed`) => void
}
declare let listener: ChangeListener
// 提示并约束为 "nameChanged" | "ageChanged"
listener.on('')
结合重映射([K in keyof T as
rename_${string & K}]
),可以实现在映射键名时基于原键名做修改:
interface Foo {
name: string
age: number
}
type CopyWithRename<T extends Object> = {
[K in keyof T as `rename_${string & K}`]: T[K];
};
type Bar = CopyWithRename<Foo>;
// {
// rename_name: string;
// rename_age: number;
// }
因为键名合法值中包含了symbol
,所以在使用模板字符串类型时,需要使用string & K
来保证键名是字符串类型。
与匹配类型 结合使用
type ReverseName<Str extends string> = Str extends `${infer First} ${infer Last}` ? `${Last} ${First}` : Str;
type ReverseNameTest = ReverseName<'Zeng Shuai'>; // Shuai Zeng