前言:这篇主要是学习ts基础语法所做的笔记,为了交流学习以及巩固复习,主要是了解TS的类型约束和内部的机制
目录
非空断言操作符(后缀 !)(Non-null Assertion Operator)
函数类型表达式(Function Type Expressions)
构造函数描述签名 (Construct Signatures)
声明类型参数 (Specifying Type Arguments)
其他需要知道的类型(Other Types to Know About)
什么是ts?
是微软出的对js的静态类型语言,也是js的超集,对js运行前的规范,避免一些类型错误
对错误的捕获:
单词的拼错,还有调用对象不存在的属性会直接报错,js则会显示underfunded
还有逻辑报错
显示类型:冒号加类型
我们将定义的变量或者参数进行类型规范。在对应的后面加上 :类型
Ts的常用类型约束:
变量上的类型注解:首先你要知道,在变量声明赋值的时候,ts能自动识别类型。可以不用去写类型约束
let str:string = ‘yy’
let str = 'yy'
Ts会自动去识别
基元类型约束(字符串,数字,布尔值):
已冒号加小写字母做变量约束
let str: string = 'yy'
let num: number = 23
let bo: boolean = true
数组:
type是类型种类,是规定数组里面的元素类型,后面带个括号,括号代表数组
let yy: number[] = [1, 2, 3]
//yy=['a'] 报错,约束了数组yy里面的元素是数值类型
let yy2: Array<Number> = [2, 3]
//同样不能修改数组里面元素的类型
对象:
- 约定对象的属性类型,已分号或者逗号分隔
- 参数对象属性的可选属性 属性后面加?,代表该属性可传可不传
- 可选属性的函数体内操作也需要判断是否传入,没传入则为undefined。
- 当你获取一个可选属性时,你需要在使用它前,先检查一下是否是
undefined
。- 不能额外传入没有在参数约束上的其他对象属性
// 参数为对象的约束,是对对象的属性进行约束
function objtest(obj: { x: number, y: number }) {
console.log(obj.x)
console.log(obj.y)
}
objtest({ x: 5, y: 7 })
// 参数对象属性的可选属性 属性后面加?
function kexuan(obj: { x: number, y?: number }) {
这个y可能会被认定为undefined,加个?表示存在的情况下使用,es6中的运算符?
console.log(obj.y?.toFixed())
}
kexuan({x:3})
下面有关于对象注解的详细介绍
any:
不希望某个特定值导致类型检查错误,可以约束为any类型
使用any类型意味着关闭了这个变量的所有类型检查,当你不想写一个长长的类型代码,仅仅想让 TypeScript 知道某段特定的代码是没有问题的,any
类型是很有用的。
下面有写和unknow类型的区分,unknow更安全,对unknow类型的使用都是非法的
函数:
函数有参数约束以及返回值类型注释
规定参数的类型
function happy(today: string) {
console.log(today)
}
happy('8月29日')
规定返回值的类型
function fanhuitest(): number{
return 26
}
fanhuitest()
函数也会存在匿名函数,这个时候他会根据上下文推断出应有的类型注解,省略人为的去操作
下面有关于函数的专门一章节讲解
联合类型:
允许使用多种运算符限制 ,使得类型约束上变得宽松,但是在代码编辑上你就不得不考虑写几种类型的判断,不然ts会爆出警告,就是类型收窄。
function test(x: string | number) {
if (typeof x == 'string') {
// todo
} else {
//todo
}
}
test(9)
test('nihao')
test([1])//提示错误
类型别名:
我们已经学会在类型注解里直接使用对象类型和联合类型,这很方便,但有的时候,一个类型会被使用多次,此时我们更希望通过一个单独的名字来引用它。
普通的类型注解直接写对应的类型注解就可以了,那样简洁明了,如果复杂的或者多次使用的肯定使用类型别名
这就是类型别名(type alias)。所谓类型别名,顾名思义,一个可以指代任意类型的名字。
使用type 命名,类型名为大写开头的字母
注意别名是唯一的别名
type Point = {
x: number;
y: number;
};
// Exactly the same as the earlier example
function printCoord(pt: Point) {
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 });
接口Interfaces:
接口声明(interface declaration)是命名结构类型的另一种方式:
TypeScript 只关心传递给函数参数的结构(structure)——关心值是否有期望的属性
interface yy {
name:string
}
function test(x: yy) {
}
test({
name:'yy'
})
接口定义不需要 等号=
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
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 });
类型别名和接口的不同
类型别名和接口非常相似,大部分时候,你可以任意选择使用。接口的几乎所有特性都可以在 type
中使用,两者最关键的差别在于类型别名本身无法添加新的属性,而接口是可以扩展的。
Interface
通过继承扩展类型
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear:Bear = {
name:'yy',
honey: true
}
bear.name
bear.honey
Type
通过交集扩展类型
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear:Bear = {
name:'yy',
honey: true
}
bear.name
bear.honey
// Interface
对一个已经存在的接口添加新的字段
interface Window {
title: string
}
interface Window {
ts: TypeScriptAPI
}
能合并
// Type
创建后不能被改变
type Window = {
title: string
}
type Window = {
ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.
类型断言(Type Assertions)
相当于类型注解一样的约束条件,也叫类型推断
有的时候,你知道一个值的类型,但 TypeScript 不知道。你可以使用类型断言将其指定为一个更具体的类型:
使用 as 加上具体的类型(或者使用尖括号),TypeScript 仅仅允许类型断言转换为一个更加具体或者更不具体的类型。这个规则可以阻止一些不可能的强制类型转换
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
第二种定义方法,在前面使用尖括号
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
可以使用双重断言,先断言为 any
(或者是 unknown
),然后再断言为期望的类型:
const a = (expr as any) as T;
字面量类型(Literal Types)
字面量类型本身并没有什么太大用:加深类型注解的约束范围,类似于js中 const的意思,不可更改
规范x变量的字面量值为hello
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.
上述规定变量的一个字面量值显然没用,因为是变量。所以我们往往配合联合类型去使用,这样就限制了变量的范围
字面量类型结合联合类型有用,就是限制多类中的一个
当函数只能传入一些固定的字符串时:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
当然了,你也可以跟非字面量类型联合:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
使用字面量类型注解会带来字面量推断的问题
字面量推断(Literal Inference)
与字面量类型相关的字面量推断,要注意起来。
当你初始化变量为一个对象的时候,TypeScript 会假设这个对象的属性的值未来会被修改
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
报错信息:req.method被识别为字符串类型 ,无法匹配字面量类型get和post
Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
上面的req.method他是可以随便更改为其他字符串的值,比如把req.method改为“yy”,那这样就会报错(不符合字面量类型约束的post或get),所以ts直接把req.method识别为string。提出报错
真的贼严格啊!
解决办法:使用类型断言,as强制将参数锁型 ,这样加了一个as const后 整个对象里面的属性值都不能改变。as const
效果跟 const
类似,但是对类型系统而言,它可以确保所有的属性都被赋予一个字面量类型,而不是一个更通用的类型比如 string
或者 number
。
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
null
和 undefined
如果一个值可能是 null
(不存在)或者 undefined(未定义)
,你需要在用它的方法或者属性之前,先检查这些值,就像用可选的属性之前,先检查一下 是否是 undefined
,我们也可以使用类型收窄(narrowing)检查值是否是 null
:
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
非空断言操作符(后缀 !
)(Non-null Assertion Operator)
!非空判断符,和js中的不同,js中!为空值判断条件
TypeScript 提供了一个特殊的语法,可以在不做任何检查的情况下,从类型中移除
null
和undefined
,这就是在任意表达式后面写上!
,这是一个有效的类型断言,表示它的值不可能是null
或者undefined
:
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
就像其他的类型断言,这也不会更改任何运行时的行为。重要的事情说一遍,只有当你明确的知道这个值不可能是
null
或者undefined
时才使用!
。
枚举(Enums)
枚举是 TypeScript 添加的新特性,用于描述一个值可能是多个常量中的一个。
声明形式像接口一样,不同的是,可以通过枚举名去调用里面的常量
如果常量赋值了,调用该常量则为常量定义的值,否则为顺序
其他:
类型缩小收窄:
只是讲解你ts运行机制,或者帮助你该如何整理你的代码逻辑
常用于处理联合类型变量的场景,一般在逻辑层用if语句等将其类型收窄
typeof 类型保护 (type guard)
在我们的 if
语句中,TypeScript 会认为 typeof padding === number
是一种特殊形式的代码,我们称之为类型保护 (type guard),TypeScript 会沿着执行时可能的路径,分析值在给定的位置上最具体的类型。
JavaScript 本身就提供了 typeof
操作符,可以返回运行时一个值的基本类型信息,会返回如下这些特定的字符串:
- "string"
- "number"
- "bigInt"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
typeof
操作符在很多 JavaScript 库中都有着广泛的应用,而 TypeScript 已经可以做到理解并在不同的分支中将类型收窄(不同类型执行不同逻辑代码)。
标准的类型收窄,分三种情况考虑写代码
function printAll(strs: string | string[] | null) {
如果是数组
if (typeof strs === "object") {
for (const s of strs) {
// Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
真值缩小:
使用逻辑运算符
无非就是一些js逻辑运算符。
等值缩小:
in操作符缩小:
在
"value" in x
中,"value"
是一个字符串字面量,而x
是一个联合类型:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
通过 "swim" in animal ,我们可以准确的进行类型收窄。
if ("swim" in animal) {
return animal.swim();
// (parameter) animal: Fish
}
return animal.fly();
// (parameter) animal: Bird
}
instanceof 收窄
instanceof
也是一种类型保护,TypeScript 也可以通过识别instanceof
正确的类型收窄:
就是js的原型链检测
分配缩小:
TypeScript 可以根据赋值语句的右值,正确的收窄左值。
也是将联合类型的范围进一步缩小
类型谓词(类型判断式):
函数的返回注解格式可以是 参数is类型注解
类型谓词的意思是函数需要返回真还是假,去根据这个谓词做判断,如果参数是fish的类型别名注解,则返回值为真(逻辑是你自己判断是否为真),否则为假,
说白了就是一个类型收窄的一个实例,就是为了让函数的返回值更加的判断精准。符合类型谓词的描述
type Fish = {
swim:string
}
type Bird =number
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let res = isFish({swim:'1'}) //打印为true
let res1 = isFish(12)//打印为false
console.log(res)
在这个例子中,pet is Fish就是我们的类型判断式,一个类型判断式采用 parameterName is Type的形式,但 parameterName 必须是当前函数的参数名。
当 isFish 被传入变量进行调用,TypeScript 就可以将这个变量收窄到更具体的类型:
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim(); // let pet: Fish
} else {
pet.fly(); // let pet: Bird
}
注意这里,TypeScript 并不仅仅知道 if
语句里的 pet
是 Fish
类型,也知道在 else
分支里,pet
是 Bird
类型,毕竟 pet
就两个可能的类型。
你也可以用 isFish
在 Fish | Bird
的数组中,筛选获取只有 Fish
类型的数组:
可辨别联合(Discriminated unions)
普通的联合字面量类型定义,比如kind为圆形时,有半径这个属性,当kind为矩形时,有边长这个属性
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
上面的两个属性是可选的,
但是我们在实际编程中,我们ts无法去判定可选属性属于哪个kind字面量,就算你收窄了,也会报错
function getArea(shape: Shape) {
if (shape.kind === "circle") {
即便我们判断 kind 是 circle 的情况,但由于 radius 是一个可选属性,TypeScript 依然会认为 radius 可能是 undefined。
return Math.PI * shape.radius ** 2;
// Object is possibly 'undefined'.
}
}
对于这种解决办法就是拆分,规定
定义两个接口,再用类型别名进行 可辩别联合
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
当联合类型中的每个类型,都包含了一个共同的字面量类型的属性,TypeScript 就会认为这是一个可辨别联合(discriminated union),然后可以将具体成员的类型进行收窄。(需要配合条件判断进行收窄)
在这个例子中,
kind
就是这个公共的属性(作为 Shape 的可辨别(discriminant) 属性 )。
never 类型
当进行收窄的时候,如果你把所有可能的类型都穷尽了,TypeScript 会使用一个
never
类型来表示一个不可能存在的状态。
never 类型可以赋值给任何类型,然而,没有类型可以赋值给 never
(除了 never
自身)。这就意味着你可以在 switch
语句中使用 never
来做一个穷尽检查。
目的: 为了防止拓展,如果有拓展,则报错。因为你已经在原有代码里面把所有的限制都弄了,这个时候再多出来一个新的,就会报错
例子:
给
getArea
函数添加一个default
,把shape
赋值给never
类型,当出现还没有处理的分支情况时,never
就会发挥作用。
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
当我们给
Shape
类型添加一个新成员,却没有做对应处理的时候,就会导致一个 TypeScript 错误:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
// Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
因为 TypeScript 的收窄特性,执行到
default
的时候,类型被收窄为Triangle
,但因为任何类型都不能赋值给never
类型,这就会产生一个编译错误。通过这种方式,你就可以确保getArea
函数总是穷尽了所有shape
的可能性。
函数在TS里如何使用:
函数是任何应用的基础组成部分,无论它是局部函数(local functions),还是从其他模块导入的函数,亦或是类中的方法。当然,函数也是值 (values),而且像其他值一样,TypeScript 有很多种方式用来描述,函数可以以怎样的方式被调用。让我们来学习一下如何书写描述函数的类型(types)。
函数类型表达式(Function Type Expressions)
函数类型表达式(function type expression)。**它的写法有点类似于箭头函数:
(参数:参数注解)=》void
语法
(a: string) => void
表示一个函数有一个名为a
,类型是字符串的参数,这个函数并没有返回任何值。
fn是回调函数,规定回调函数的类型注解
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
如果一个函数参数的类型并没有明确给出,它会被隐式设置为 any
。
注意函数参数的名字是必须的,这种函数类型描述
(string) => void
,表示的其实是一个函数有一个类型是any
,名为string
的参数。
当然了,我们也可以使用类型别名(type alias)定义一个函数类型:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
函数描述:
然而上一节讲到的函数类型表达式并不能支持声明属性,如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature)。
我们可以用一个属性描述该函数,格式如下
将函数注解定义为一个对象
type DescribableFunction = {
description: string;
(someArg: number): boolean; 函数表达式原本的=》改为:
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
注意这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是
:
而不是=>
。
构造函数描述签名 (Construct Signatures)
JavaScript 函数也可以使用
new
操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为他们会产生一个新对象。你可以写一个构造签名,方法是在调用签名前面加一个new
关键词:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
泛型函数 (Generic Functions)
函数的输出类型依赖函数的输入类型,或者两个输入的类型以某种形式相互关联。让我们考虑这样一个函数,它返回数组的第一个元素:
function firstElement(arr: any[]) {
return arr[0];
}
注意此时函数返回值的类型是 any
,如果能返回第一个元素的具体类型就更好了。
在 TypeScript 中,泛型就是被用来描述两个值之间的对应关系。我们需要在函数签名里声明一个类型参数 (type parameter):来描述泛型
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
记住,所谓泛型就是用一个相同类型来关联两个或者更多的值。
声明类型参数 (Specifying Type Arguments)
TypeScript 通常能自动推断泛型调用中传入的类型参数,但也并不能总是推断出。举个例子,有这样一个合并两个数组的函数:
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
如果你像下面这样调用函数就会出现错误:
const arr = combine([1, 2, 3], ["hello"]);
// Type 'string' is not assignable to type 'number'.
而如果你执意要这样做,你可以手动指定 Type
:
const arr = combine<string | number>([1, 2, 3], ["hello"]);
写一个好的泛型函数的一些建议
- 如果可能的话,直接使用类型参数而不是约束它
- 尽可能用更少的类型参数
- 如果一个类型参数仅仅出现在一个地方,强烈建议你重新考虑是否真的需要它
- 类型参数应该出现两次,这样才算关联,否则一次的话,就不会有啥关联
可选参数(Optional Parameters)
我们可以使用 ?
表示这个参数是可选的:
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
尽管这个参数被声明为 number
类型,x
实际上的类型为 number | undefiend
,这是因为在 JavaScript 中未指定的函数参数就会被赋值 undefined
。
你当然也可以提供有一个参数默认值:
function f(x = 10) {
// ...
}
当你写一个回调函数的类型时,不要写一个可选参数, 除非你真的打算调用函数的时候不传入实参
其他需要知道的类型(Other Types to Know About)
这里介绍一些也会经常出现的类型。像其他的类型一样,你也可以在任何地方使用它们,但它们经常与函数搭配使用。
void
void
表示一个函数并不会返回任何值,当函数并没有任何返回值,或者返回不了明确的值的时候,就应该用这种类型。
// The inferred return type is void
function noop() {
return;
}
在 JavaScript 中,一个函数并不会返回任何值,会隐式返回 undefined
,但是 void
和 undefined
在 TypeScript 中并不一样。在本文的最后会有更详细的介绍。
void 跟 undefined 不一样
void还有个注意的点,
当你是函数类型注解的时候,或者使用类型别名定义函数的时候,并不会强制函数一定不能返回内容。
type voidFunc = () => void;
这样是允许的
const f1: voidFunc = () => {
return true;
};
const f2: voidFunc = () => true;
const f3: voidFunc = function () {
return true;
};
如果是说是函数的返回值定义为void,函数是一定不能返回任何东西的:这个时候会报错
function f2(): void {
// @ts-expect-error
return true;
}
const f3 = function (): void {
// @ts-expect-error
return true;
};
object
这个特殊的类型 object
可以表示任何不是原始类型(primitive)的值 (string
、number
、bigint
、boolean
、symbol
、null
、undefined
)。object
不同于空对象类型 { }
,也不同于全局类型 Object
。很有可能你也用不到 Object
。
object 不同于
Object
,总是用object
!
注意在 JavaScript 中,函数就是对象,他们可以有属性,在他们的原型链上有 Object.prototype
,并且 instanceof Object
。你可以对函数使用 Object.keys
等等。由于这些原因,在 TypeScript 中,函数也被认为是 object
。
unknown
unknown
类型可以表示任何值。有点类似于 any
,但是更安全,因为对 unknown
类型的值做任何事情都是不合法的:
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
// Object is of type 'unknown'.
}
有的时候用来描述函数类型,还是蛮有用的。你可以描述一个函数可以接受传入任何值,但是在函数体内又不用到 any
类型的值。
你可以描述一个函数返回一个不知道什么类型的值,比如:
function safeParse(s: string): unknown {
return JSON.parse(s);
}
// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);
never
一些函数从来不返回值:
function fail(msg: string): never {
throw new Error(msg);
}
never
类型表示一个值不会再被观察到 (observed)。
作为一个返回类型时,它表示这个函数会丢一个异常,或者会结束程序的执行。
当 TypeScript 确定在联合类型中已经没有可能是其中的类型的时候,never
类型也会出现:
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
Function
在 JavaScript,全局类型 Function
描述了 bind
、call
、apply
等属性,以及其他所有的函数值。
它也有一个特殊的性质,就是 Function
类型的值总是可以被调用,结果会返回 any
类型:
function doSomething(f: Function) {
f(1, 2, 3);
}
这是一个无类型函数调用 (untyped function call),这种调用最好被避免,因为它返回的是一个不安全的 any
类型。
如果你准备接受一个黑盒的函数,但是又不打算调用它,() => void
会更安全一些。
剩余参数
除了用可选参数、重载能让函数接收不同数量的函数参数,我们也可以通过使用剩余参数语法(rest parameters),定义一个可以传入数量不受限制的函数参数的函数:
剩余参数必须放在所有参数的最后面,并使用
...
语法:
剩余参数要写成数组类型注解
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
在 TypeScript 中,剩余参数的类型会被隐式设置为
any[]
而不是any
,如果你要设置具体的类型,必须是Array<T>
或者T[]
的形式,再或者就是元组类型(tuple type)。
参数解构(Parameter Destructuring)
你可以使用参数解构方便的将作为参数提供的对象解构为函数体内一个或者多个局部变量
无非就是写对象类型注解一样的,只是参数写成结构的样子
在解构语法后,要写上对象的类型注解:
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });
这个看起来有些繁琐,你也可以这样写:
// 跟上面是有一样的
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
不要在解构对象里面去使用类型注解,那样会和js的解构赋值矛盾。正确写法如上,在整个对象外面写注解约束
详解对象类型(Object types)
在 JavaScript 中,最基本的将数据成组和分发的方式就是通过对象。在 TypeScript 中,我们通过对象类型(object types)来描述对象。
一般使用接口命名或者类型别名type
属性修饰符
对象可选属性
我们讲过,在属性后面带❓,一般我们在逻辑里面要考虑这个可选属性为undefined的情况。
对于js解构对象默认值的情况,则和我们定义对象类型注解不一致。
注意现在并没有在解构语法里放置类型注解的方式。
interface PaintOptions {
shape: string;
xPos?: number;
yPos?: number;
}
参数默认值
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos); // (parameter) xPos: number
console.log("y coordinate at", yPos); // (parameter) yPos: number
}
paintShape({shape:'r',xPos:5})
readonly
属性
只读属性
在 TypeScript 中,属性可以被标记为 readonly
,这不会改变任何运行时的行为,但在类型检查的时候,一个标记为 readonly
的属性是不能被写入的。
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}
属性的值为引用类型:
不过使用 readonly
并不意味着一个值就完全是不变的,亦或者说,内部的内容是不能变的。readonly
仅仅表明属性本身是不能被重新写入的。
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
不能改这个属性
home.resident = {
// Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}
readonly
的值是可以通过别名修改的。
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
索引签名:
看视频学吧
属性合并:
属性继承
对接口使用
extends
关键字允许我们有效的从其他声明过的类型中拷贝成员,并且随意添加新成员。接口也可以继承多个类型:
扩展接口属性注解,
通过定义新的接口名继承老接口
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
交叉类型:
合并已经存在的对象类型,
交叉类型的定义需要用到
&
操作符
1:通过多个继承合并
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
2:交叉类型的定义需要用到 & 操作符
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
也可以不用写别名,直接
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
合并方法的不同:
- 使用继承的方式,如果重写相同的属性名字会导致编译错误,但交叉类型不会:
interface Colorful { color: string; } type ColorfulSub = Colorful & { color: number }
使用交叉类型虽然不会报错,那
color
属性的类型是什么呢,答案是never
,取得是string
和number
的交集。
泛型对象类型
泛型就是怎么说呢,,关于类型注解之间的联系,可以说类型是泛泛而云,但必须有所关联
比如我们规定一个对象属性的类型
interface Box {
contents: any;
}
- 这个content可以是任何类型,也可以写联合类型进行收窄
- 但是那样的话要做很多逻辑分析处理
- 我们可以使用泛型约束
- 只要到时候我们只要手动指定type就可以了
规定泛型接口
interface Box<Type> {
contents: Type;
}
你可以这样理解:Box
的 Type
就是 contents
拥有的类型 Type
。
当我们引用 Box
的时候,我们需要给予一个类型实参替换掉 Type
:
let box: Box<string>;
把 Box
想象成一个实际类型的模板,Type
就是一个占位符,可以被替代为具体的类型。当 TypeScript 看到 Box<string>
,它就会替换为 Box<Type>
的 Type
为 string
,最后的结果就会变成 { contents: string }
。换句话说,Box<string>
和 StringBox
是一样的。
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
// (property) Box<string>.contents: string
let boxB: StringBox = { contents: "world" };
boxB.contents;
// (property) StringBox.contents: string
不过现在的 Box
是可重复使用的,如果我们需要一个新的类型,我们完全不需要再重新声明一个类型。
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
这也意味着我们可以利用泛型函数避免使用函数重载。
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}
类型别名也是可以使用泛型的。比如接口这么写:
interface Box<Type> {
contents: Type;
}
使用别名对应就是:
type Box<Type> = {
contents: Type;
};
类型别名不同于接口,可以描述的不止是对象类型,所以我们也可以用类型别名写一些其他种类的的泛型帮助类型。
type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
type OneOrManyOrNullStrings = OneOrMany<string> | null
详解 Array
类型
array其实也是对象,不过我单独拿出来说
我们之前讲过
Array
类型,当我们这样写类型number[]
或者string[]
的时候,其实它们只是Array<number>
和Array<string>
的简写形式而已。
其实array的类型注解也属于泛型。
现代 JavaScript 也提供其他是泛型的数据结构,比如
Map<K, V>
,Set<T>
和Promise<T>
。因为Map
、Set
、Promise
的行为表现,它们可以跟任何类型搭配使用。
只读Array
类型
ReadonlyArray
是一个特殊类型,它可以只读,但是数组不能被改变。
ReadonlyArray
主要是用来做意图声明。当我们看到一个函数返回ReadonlyArray
,就是在告诉我们不能去更改其中的内容,当我们看到一个函数支持传入ReadonlyArray
,这是在告诉我们我们可以放心的传入数组到函数中,而不用担心会改变数组的内容。
let arr : ReadonlyArray<string>=["yy"]
1:他不是构造函数
new ReadonlyArray("red", "green", "blue");
// 'ReadonlyArray' only refers to a type, but is being used as a value here.
2:可以把常规值赋值给只读数组
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
3:TypeScript 也针对 ReadonlyArray<Type>
提供了更简短的写法 readonly Type[]
。就是加上readonly关键词
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
// Property 'push' does not exist on type 'readonly string[]'.
}
4:不能把只读数组赋值给别人,但是别的数组可以赋值给只读数组,不是双向的
let x: readonly string[] = [];
let y: string[] = [];
x = y; // ok
y = x; // The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
元组类型
元组类型是另外一种
Array
类型,当你明确知道数组包含多少个元素,并且每个位置元素的类型都明确知道的时候,就适合使用元组类型。
type StringNumberPair = [string, number];
在这个例子中,StringNumberPair
就是 string
和 number
的元组类型。
跟 ReadonlyArray
一样,它并不会在运行时产生影响,但是对 TypeScript 很有意义。因为对于类型系统,StringNumberPair
描述了一个数组,索引 0 的值的类型是 string
,索引 1 的值的类型是 number
。
我们也可以使用 JavaScript 的数组解构语法解构元组:
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString); // const inputString: string
console.log(hash); // const hash: number
}
元组类型在重度依赖约定的 API 中很有用,因为它会让每个元素的意义都很明显。当我们解构的时候,元组给了我们命名变量的自由度。在上面的例子中,我们可以命名元素
0
和1
为我们想要的名字。
元组的可选属性?
在元组类型中,你也可以写一个可选属性,但可选元素必须在最后面,而且也会影响类型的 length
。
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
// const z: number | undefined
console.log(`Provided coordinates had ${coord.length} dimensions`);
// (property) length: 2 | 3
}
Tuples 也可以使用剩余元素语法,但必须是 array/tuple 类型:有剩余元素的元组并不会设置 length
,因为它只知道在不同位置上的已知元素信息:
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
可选元素和剩余元素的存在,使得 TypeScript 可以在参数列表里使用元组,就像这样:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
基本等同于:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
只读元组类型:
在大部分的代码中,元组只是被创建,使用完后也不会被修改,所以尽可能的将元组设置为
readonly
是一个好习惯。
TypeScript 就不会允许写入readonly
元组的任何属性:
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
// Cannot assign to '0' because it is a read-only property.
}
如果我们给一个数组字面量 const
断言,也会被推断为 readonly
元组类型。
let point = [3, 4] as const;
泛型:
我们在上面讲解了很多关于泛型的用例,这一篇我们详细讲解
软件工程也需要是可复用的(reusable)。好的组件不仅能够兼容今天的数据类型,也能适用于未来可能出现的数据类型,这在构建大型软件系统时会给你最大的灵活度。
用来创建可复用组件的工具,我们称之为泛型(generics)。利用泛型,我们可以创建一个支持众多类型的组件,这让用户可以使用自己的类型消费(consume)这些组件。
设置泛型参数
所以我们需要一种可以捕获参数类型的方式,然后再用它表示返回值的类型。这里我们用了一个类型变量(type variable),一种用在类型而非值上的特殊的变量Type。这个 Type
允许我们捕获用户提供的类型,使得我们在接下来可以使用这个类型
设置type变量
function identity<Type>(arg: Type): Type {
return arg;
}
设置变量,约定类型约束
let output = identity<string>("myString"); // let output: string
当然我们有时候不需要指定这个<>括号里面的type类型,因为会有类型推断机制。
let output = identity("myString"); // let output: string
注意这次我们并没有用 <>
明确的传入类型,当编译器看到 myString
这个值,就会自动设置 Type
为它的类型(即 string
)。
而在一些更加复杂的例子中,当编译器推断类型失败,你才需要像上一个例子中那样,明确的传入参数。
泛型对象接口
上面有讲过
除了泛型接口之外,我们也可以创建泛型类。注意,不可能创建泛型枚举类型和泛型命名空间。
泛型约束
通过继承接口的特性,降低泛型的范围
在早一点的 loggingIdentity
例子中,我们想要获取参数 arg
的 .length
属性,但是编译器并不能证明每种类型都有 .length
属性,所以它会提示错误:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
相比于能兼容任何类型,我们更愿意约束这个函数,让它只能使用带有 .length
属性的类型。只要类型有这个成员,我们就允许使用它,但必须至少要有这个成员。为此,我们需要列出对 Type
约束中的必要条件。
为此,我们需要创建一个接口,用来描述约束。这里,我们创建了一个只有
.length
属性的接口,然后我们使用这个接口和extends
关键词实现了约束:
定义接口
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;
}
现在这个泛型函数被约束了,它不再适用于所有类型:
loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
我们需要传入符合约束条件的值:
loggingIdentity({ length: 10, value: 3 });
操作符:
包括keyof和typeof
keyof
类型操作符
返回对象或者类或者接口的属性,生成已该属性为约束的字面量类型联合注解
对一个对象类型使用 keyof
操作符,会返回该对象属性名组成的一个字符串或者数字字面量的联合。这个例子中的类型 P 就等同于 "x" | "y":
type Point = { x: number; y: number };
type P = keyof Point;
// type P = "x" | "y"
实战
在「泛型 (opens new window)」这篇中就讲到了一个 keyof
的应用:
我们希望获取一个对象给定属性名的值,为此,我们需要确保我们不会获取 obj
上不存在的属性。所以我们在两个类型之间建立一个约束:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
typeof
类型操作符
获取类型:
JavaScript 本身就有 typeof
操作符,你可以在表达式上下文中(expression context)使用:
// Prints "string"
console.log(typeof "Hello world");
而 TypeScript 添加的 typeof
方法可以在类型上下文(type context)中使用,用于获取一个变量或者属性的类型。
let s = "hello";
let n: typeof s;
// let n: string
对对象使用 typeof
我们可以对一个对象使用 typeof
:是对 对象里面的属性给返回类型限制
const person = { name: "kevin", age: "18" }
type Kevin = typeof person;
// type Kevin = {
// name: string;
// age: string;
// }
对函数使用 typeof
我们也可以对一个函数使用 typeof
:返回函数的类型注解
function identity<Type>(arg: Type): Type {
return arg;
}
type result = typeof identity;
// type result = <Type>(arg: Type) => Type
对 enum 使用 typeof
在 TypeScript 中,enum 是一种新的数据类型,但在具体运行的时候,它会被编译成对象。
enum UserResponse {
No = 0,
Yes = 1,
}
type result = typeof UserResponse;
// ok
const a: result = {
"No": 2,
"Yes": 3
}
result 类型类似于:
// {
// "No": number,
// "YES": number
// }
不过对一个 enum 类型只使用 typeof 一般没什么用,通常还会搭配 keyof 操作符用于获取属性名的联合字符串:
type result = keyof typeof UserResponse;
// type result = "No" | "Yes"
四大类型详解:
索引访问类型:
这里有个对象的索引签名还没弄
条件类型:
映射类型:
模板字面量类型:
类:
和es6的类差不多
模块:
和es6的模块差不多
基础篇收尾,至于后面的进阶篇,关于装饰器等内容,我这会去学vue3了,没有遇到大型的ts项目,学了也会忘记