GVKun编程网logo

TypeScript高级类型备忘录(附示例)(高级备忘录下载)

26

对于TypeScript高级类型备忘录感兴趣的读者,本文将会是一篇不错的选择,我们将详细介绍附示例,并为您提供关于javaScript引用类型的详细介绍(附示例)、JavaScript的数据类型与变量

对于TypeScript高级类型备忘录感兴趣的读者,本文将会是一篇不错的选择,我们将详细介绍附示例,并为您提供关于javaScript引用类型的详细介绍(附示例)、JavaScript的数据类型与变量的解析(附示例)、TypeScript 内置高级类型编程示例、TypeScript 高级类型的有用信息。

本文目录一览:

TypeScript高级类型备忘录(附示例)(高级备忘录下载)

TypeScript高级类型备忘录(附示例)(高级备忘录下载)

点上方蓝字关注公众号「前端从进阶到入院」,精选原创好文助你进入大厂。


原文链接: https://dev.to/ibrahima92/advanced-typescript-types-cheat-sheet-with-examples-5414

经作者授权后翻译

TypeScript 是一种类型化的语言,允许你指定变量的类型,函数参数,返回的值和对象属性。

这里是一个带有示例的高级 TypeScript 类型备忘单。

1. 交叉类型

交叉类型是将多种类型组合为一种类型的方法。这意味着你可以将给定的类型A与类型B或更多类型合并,并获得具有所有属性的单个类型。

type LeftType = {
    id: number
    left: string
}

type RightType = {
    id: number
    right: string
}

type IntersectionType = LeftType & RightType

function showType(args: IntersectionType{
    console.log(args)
}

showType({
    id1left"test"right"test"
})
// Output: {id: 1, left: "test", right: "test"}

IntersectionType 组合了两种类型: LeftTypeRightType ,并使用 符号构造交叉类型。

2. 联合类型

联合类型表示一个值可以是几种类型之一,例如某个函数希望传入 string 或者 number 类型的参数。

type UnionType = string | number

function showType(arg: UnionType{
    console.log(arg)
}

showType("test")
// Output: test

showType(7)
// Output: 7

函数 showType 的参数是一个联合类型,它接受 stringnumber 作为参数。

3.泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

function showType<T>(args: T{
    console.log(args)
}

showType("test")
// Output: "test"

showType(1)
// Output: 1

要构造泛型,需要使用尖括号并将 T 作为参数传递。
在这里,我使用 T(名称自定义),然后使用不同的类型两次调用 showType 函数。

interface GenericType<T> {
    id: number
    name: T
}

function showType(args: GenericType<string>{
    console.log(args)
}

showType({ id1name"test" })
// Output: {id: 1, name: "test"}

function showTypeTwo(args: GenericType<number>{
    console.log(args)
}

showTypeTwo({ id1name4 })
// Output: {id: 1, name: 4}

在这里,我们有另一个示例,该示例具有一个接口 GenericType,该接口接收泛型 T 。由于它是可重用的,因此可以首先使用字符串,然后使用数字来调用它。

interface GenericType<T, U> {
    id: T
    name: U
}

function showType(args: GenericType<number, string>{
    console.log(args)
}

showType({ id1name"test" })
// Output: {id: 1, name: "test"}

function showTypeTwo(args: GenericType<string, string[]>{
    console.log(args)
}

showTypeTwo({ id"001"name: ["This""is""a""Test"] })
// Output: {id: "001", name: Array["This", "is", "a", "Test"]}

泛型可以接收多个参数。在这里,我们传入两个参数:TU,然后将它们用作属性的类型。也就是说,我们现在可以使用该接口并提供不同的类型作为参数。

内置类型

TypeScript 提供了方便的内置类型,可帮助轻松地操作类型。要使用它们,你需要将要转换的类型传递给 <>

Partial

  • Partial

Partial 允许你将 T 类型的所有属性设为可选。

interface PartialType {
    id: number
    firstName: string
    lastName: string
}

function showType(args: Partial<PartialType>{
    console.log(args)
}

showType({ id1 })
// Output: {id: 1}

showType({ firstName"John"lastName"Doe" })
// Output: {firstName: "John", lastName: "Doe"}

如你所见,我们有一个 PartialType 接口,用作 showType() 函数接收的参数的类型。为了使属性成为可选属性,我们必须使用 Partial 关键字并将 PartialType 类型作为参数传递。也就是说,现在所有字段都变为可选。

Required

  • Required

Partial 不同,Required 所有类型为 T 的属性成为必需。

interface RequiredType {
    id: number
    firstName?: string
    lastName?: string
}

function showType(args: Required<RequiredType>{
    console.log(args)
}

showType({ id1firstName"John"lastName"Doe" })
// Output: { id: 1, firstName: "John", lastName: "Doe" }

showType({ id1 })
// Error: Type ''{ id: number: }'' is missing the following properties from type ''Required<RequiredType>'': firstName, lastName

即使我们之前设置的是可选属性,Required 也会使所有属性成为必需。如果省略属性,TypeScript 会抛出错误。

Readonly

  • Readonly

ReadonlyT 类型的所有属性变成只读属性。

interface ReadonlyType {
    id: number
    name: string
}

function showType(args: Readonly<ReadonlyType>{
    args.id = 4
    console.log(args)
}

showType({ id1name"Doe" })
// Error: Cannot assign to ''id'' because it is a read-only property.

这里,我们使用 Readonly 来使 ReadonlyType 的属性变成只读属性。如果你尝试为这些字段赋值,则会引发错误。

除此之外,还可以在属性前面使用关键字 readonly 使其只读。

interface ReadonlyType {
    readonly id: number
    name: string
}

Pick

  • Pick

T 中取出 K 中指定的属性。

interface PickType {
    id: number
    firstName: string
    lastName: string
}

function showType(args: Pick<PickType, "firstName" | "lastName">{
    console.log(args)
}

showType({ firstName"John"lastName"Doe" })
// Output: {firstName: "John"}

showType({ id3 })
// Error: Object literal may only specify known properties, and ''id'' does not exist in type ''Pick<PickType, "firstName" | "lastName">''

Pick 需要两个参数 —— T 是要从中选择元素的类型,而 K 是要选择的属性。你也可以通过使用竖线( | )分隔多个字段来选择多个字段。

Omit

  • Omit

OmitPick 相反,不是选择元素,而是从类型 T 中删除 K 属性。

interface PickType {
    id: number
    firstName: string
    lastName: string
}

function showType(args: Omit<PickType, "firstName" | "lastName">{
    console.log(args)
}

showType({ id7 })
// Output: {id: 7}

showType({ firstName"John" })
// Error: Object literal may only specify known properties, and ''firstName'' does not exist in type ''Pick<PickType, "id">''

Extract

  • Extract

ExtractT 中提取所有可分配给 U 的类型。

interface FirstType {
    id: number
    firstName: string
    lastName: string
}

interface SecondType {
    id: number
    address: string
    city: string
}

type ExtractType = Extract<keyof FirstType, keyof SecondType>
// Output: "id"

在这里,我们有两种共同的属性 id。因此,通过使用 Extract 关键字,由于两个接口中都存在字段 id,因此我们可以获取它。并且,如果有有多个共同字段,Extract 将提取所有共同的属性。

Exclude

Extract 不同,ExcludeT 中排除所有可分配给 U 的类型。

interface FirstType {
    id: number
    firstName: string
    lastName: string
}

interface SecondType {
    id: number
    address: string
    city: string
}

type ExcludeType = Exclude<keyof FirstType, keyof SecondType>

// Output; "firstName" | "lastName"

如上所示,属性 firstNamelastName 可分配给 ExcludeType 类型,因为它们在 SecondType 中不存在。

Record

  • Record

Record<K,T> 构造具有给定类型 T 的一组属性 K 的类型。在将一个类型的属性映射到另一个类型的属性时,Record 非常方便。

interface EmployeeType {
    id: number
    fullname: string
    role: string
}

let employees: Record<number, EmployeeType> = {
    0: { id1fullname"John Doe"role"Designer" },
    1: { id2fullname"Ibrahima Fall"role"Developer" },
    2: { id3fullname"Sara Duckson"role"Developer" },
}

// 0: { id: 1, fullname: "John Doe", role: "Designer" },
// 1: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
// 2: { id: 3, fullname: "Sara Duckson", role: "Developer" }

Record 的工作方式相对简单。在这里,它期望数字作为类型,属性值的类型是 EmployeeType ,因此具有 idfullNamerole 字段的对象。

NonNullable

  • NonNullable

NonNullable 从类型T中删除 nullundefined

type NonNullableType = string | number | null | undefined

function showType(args: NonNullable<NonNullableType>{
    console.log(args)
}

showType("test")
// Output: "test"

showType(1)
// Output: 1

showType(null)
// Error: Argument of type ''null'' is not assignable to parameter of type ''string | number''.

showType(undefined)
// Error: Argument of type ''undefined'' is not assignable to parameter of type ''string | number''.

在这里,我们将类型 NonNullableType 作为参数传递给 NonNullableNonNullable 将该类型中排除 nullundefined 来构造新类型。也就是说,如果传递可为空的值,TypeScript 将报错。

顺便说一句,如果将 --strictNullChecks 标志添加到 tsconfig 文件,TypeScript 将应用非空性规则。

Mapped types

映射类型允许你采用现有模型并将其每个属性转换为新类型。

type StringMap<T> = {
    [P in keyof T]: string
}

function showType(arg: StringMap<{ id: number; name: string }>{
    console.log(arg)
}

showType({ id1name"Test" })
// Error: Type ''number'' is not assignable to type ''string''.

showType({ id"testId"name"This is a Test" })
// Output: {id: "testId", name: "This is a Test"}

StringMap<> 会将传入的任何类型转换为字符串。就是说,如果我们在函数 showType() 中使用它,则接收到的参数必须是字符串,否则,TypeScript 将报错。

类型保护

类型保护使你可以使用运算符检查变量或对象的类型。

typeof
function showType(x: number | string{
    if (typeof x === "number") {
        return `The result is ${x + x}`
    }
    throw new Error(`This operation can''t be done on a ${typeof x}`)
}

showType("I''m not a number")
// Error: This operation can''t be done on a string

showType(7)
// Output: The result is 14

我们有一个普通的 JavaScript 条件块,使用 typeof 来检查入参的类型。

instanceof

instanceof 类型保护是通过构造函数来细化类型的一种方式。

class Foo {
    bar() {
        return "Hello World"
    }
}

class Bar {
    baz = "123"
}

function showType(arg: Foo | Bar{
    if (arg instanceof Foo) {
        console.log(arg.bar())
        return arg.bar()
    }

    throw new Error("The type is not supported")
}

showType(new Foo())
// Output: Hello World

showType(new Bar())
// Error: The type is not supported
in
interface FirstType {
    x: number
}
interface SecondType {
    y: string
}

function showType(arg: FirstType | SecondType{
    if ("x" in arg) {
        console.log(`The property ${arg.x} exists`)
        return `The property ${arg.x} exists`
    }
    throw new Error("This type is not expected")
}

showType({ x7 })
// Output: The property 7 exists

showType({ y"ccc" })
// Error: This type is not expected

条件类型

它测试两种类型,并根据该测试的结果选择其中一种。

type NonNullable<T> = T extends null | undefined ? never : T

NonNullable 检查类型是否为 null  或者 undefined,并根据检查结果进行处理。

谢谢阅读~

感谢大家

1.喜欢的话别忘了分享、点赞、在看三连哦~。

2.长按下方图片,关注「前端从进阶到入院」,获取我分类整理的原创精选面试热点文章,Vue、React、TS、手写题等应有尽有,我进入字节、滴滴、百度的小伙伴们说文章对他们帮助很大,希望也能帮助到你。


本文分享自微信公众号 - 前端从进阶到入院(code_with_love)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

javaScript引用类型的详细介绍(附示例)

javaScript引用类型的详细介绍(附示例)

本篇文章给大家带来的内容是关于javascript引用类型的详细介绍(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

概念 : 引用类型是一种数据结构,用于将数据和功能组织在一起,也就是 类;
对象为特殊的引用类型实例。新对象由new后加一个构造函数创建的。
构造函数:用于创建对象的函数。
例 : var person = new Object();
object为构造函数,为新对象提供默认属性与方法。

1、object类型

//普通方法

var person = new Object();
person.name = "syr";
person.age = 22;
登录后复制

//对象字面量

var person = {    
    name : "syr",    
    age : 22,    
    5 : true            //5自动转为"5"
}
* 数值属性名会自动转为字符串;
* 对象字面量推荐只在属性名可读的情况下使用,也是向函数传递大量参数首选方式,必须使用命名参数,对象字面量封装多个可选参数。
* 访问对象一般用点表示法,js中也可以用方括号法,将要访问的属性以字符串的形式放进去。
登录后复制

例 : alert(person["name"]); // 等于 person.name

立即学习“Java免费学习笔记(深入)”;

* 方括号法主要优点是可以通过变量来访问属性。
* 点表示法属性名不能包含错误的自负或者保留字和关键字,但方括号法可以。
* 通常,除非必须使用变量来访问属性,否则推荐使用点表示法。
登录后复制

2、Array类型:

* 数组:数据的有序列表。
登录后复制

es中的数组每一项都可以保存任何类型的数据,数组大小也可以动态调整。

方法一 : new可以省略;
var colors = new Array();
方法二 : 数组字面量
var colors = ["red","blue","green"];
登录后复制

与对象一样,字面量不会调用Array构造函数。

var colors = ["red","blue","green"];
colors[0] // 显示第一项
colors[2] = "black"; // 修改第二项
colors[3] = "brown" ; //增加第四项
colors.length = "black" ;  增加一项
登录后复制

length属性可返回数组的长度,可增加和移除数组的项。

* 检测数组

value instanceof Array // 判断是否为数组
Array.isArray(value) // isArray确定到底是不是数组,不管在哪个环境中创建
* 转换方法
登录后复制

所有对象都具有toLocalString()方法,后台调用tostring(),null和undefined返回结果以空字符串表示。

* 栈方法 : 后进先出(吃了吐)
登录后复制

push() : 接受任意数量参数,添加到末尾;
pop() : 移除数组末尾一项。

* 队列方法 : 先进先出(吃了拉);
登录后复制

shift() : 前端移除项并返回该项,长度减1;
unshift() : 前端推入一项;

* 重排序方法
登录后复制

reverse() : 逆序;
sort() : 顺序;
比较的是字符串,会自动转为字符串。
如 "10" 位于"5"的前面,因此应传给sort一个比较函数。

function compare(value1,value2){    
    if(value1  value2){        
    return 1;    
    }else{        
    return 0;    
    }
    }
value = [0,1,5,10,15];value.sort(value);  //0,1,5,10,15
* 操作方法
登录后复制

concat()拼接 : 先把原数组复制一个,然后把参数添加至末尾,原数组不变。slice(1,2) : 剪切:返回参数1起始位置,参数2为结束位置,包前不包后,原数组不变。solice() : 主要向数组中插入项。(数组操作最主要的方法):
-删除 : 可删除任意数量项,制定两个参数: arg1位要删除的位置,arg2为要删除的项数,例:splice(0,2) : 删除前两项;
-插入 : 三个参数。起始位置,删除项数和要插入的项,可在后面插入多个项。例 : solice(2,0,"red","green");从位置2插入"red"和"green".
-替换 : 同上,插入项不必与删除项数相等。
注意 : splice()会改变原始数组;

* 位置方法
登录后复制

indexof() : 从头查找
lastIndexof() : 从未查找
arg1表示要查找的项,arg2为起始位置(可选参数),没找到返回-1,比较时使用的是全等。

var number = [1,2,3,4,5,4,3,2,1];
number.indexof(4) ;  //返回5

* 迭代方法
登录后复制

every();
filter();
forEach();
map();

* 归并方法
登录后复制

reduce() : 从头
reduceRight ; 从尾
都会迭代数组所有项,构建一个最终返回项。

3、Date类型

var now = new Data();
date.parse("May 25,2004"); // 新建特定日期
* 继承: 重写了3个方法
登录后复制

toLocalString() : 浏览器设置日期;
toString();
valueof();

* 日期格式化方法:
登录后复制

-toDateString() : 特定格式显示星期、月、日、年;
-toTimeString() : 时分秒,时区;
toLocalDateString() : 按地区显示星期、月、日、年;
toLocalTimeString() : 时分秒;
toUTCString() : 特定格式显示完整UTC日期,具体显示因浏览器而异。

4、RegExp类型: 正则表达式;

var expression = /pattern/flags;
标志flags包括 :
g : 全局;
i : 不区分大小写;
m : 多行模式;
例子 : var pattern = /at/g 全局找at
var pattern = /[bc]at/i ; 匹配第一个bat或cat,不区分大小写。
无意符须转义;
* RegExp实例方法:
登录后复制

RegExp主要方法是
-exec():捕获组,参数为一个字符串,返回数组,无匹配返回null,返回多两个额外属性:index 和 input。
index : 位置 input:捕获的字符串
-test():匹配则返回true,常用来验证用户输入。

5、function类型

* 概念
登录后复制

函数实际就是对象,与其他引用类型一样,有属性和方法,函数名就是只想对象的指针。

* 没有重载
登录后复制

声明相同函数时,后面的会覆盖前面的函数。

* 函数声明和函数表达式
function sum(){    }    // 函数声明
var sum = function(){    }      // 函数表达式函数声明可以变量提升,任何时候任何位置都可以调用。
* 作为值的函数
登录后复制

把函数作为参数传给另一个函数,要访问函数而不执行函数,必须去掉函数名后面的那对圆括号
// 从一个函数返回另一个函数
// 根据某个对象数组排序
function compare(pro){

return function(obj1,obj2){        
var val1 = obj1[pro];        
var val2 = obj2[pro];        
if(val1 &gt; val2){            
return 1;        
}else if(val1 <p>内部含有两个特殊对象:this和argument<br>-argument:包含着传入的所有参数,内有callee属性,是一个指针,只想拥有此对象的函数。<br>// 递归算法算阶乘<br>function fac(num){</p><pre>if(num <p>函数也是对象,因此有属性和方法;</p><pre>* -属性 : length 和 prototype
登录后复制

-length 表示希望接收命名参数的个数。
-prototype 是保存所有势力和方法的真正所在,如里面保存着tostring(),valueOf等,也可以用来继承,极为重要。
prototype属性不可枚举,因此不能用for-in。

* -方法 : 非继承而来的方法有两个: apply() 和 call().
登录后复制

作用为在特定的作用域中调用函数,实际是设置函数体内this对象的值。两个方法的作用相同,区别为接收参数的方式不同。call() : 参数一一列举
apply() : 参数为一个数组两者的作用不止是传递参数,真正的作用是扩充函数的作用域。把访问不到的地方传进来。
bind():方法,创建函数实例,与this值绑定,全局作用域。

6、基本包装类型 (也是对象)

为方便操作基本类型值,提供3个特殊引用类型:Boolean,Number和String.
每读取值后台都会创建对应的基本包装类型的对象,才可以用方法操作数据。
引用类型与基本包装类型主要区别为对象生存期,用new创建引用累心实例一直保存在内存中,自动创建基本包装类型存在执行瞬间,然后立即销毁,所以不能给基本类型添加属性和方法。

* Boolean 类型
登录后复制

typeof 基本类型 // ‘boolean’typeof 引用类型 //  ''object''建议永不要使用Boolean对象

* Number 类型
登录后复制

toFixed()方法 : 按照指定方法返回数值的字符串表示。
var num = 10;num.tpFixed(2) // ''10.00''
toExpoential() : 幂

* String 类型
登录后复制

属性:length : 表示含多少个字符方法:
1)字符方法:charAt()和charCodeAt():查找某个字符在字符串中的位置。    charCodeAt():查找字符编码
2)字符串操作方法:拼接用+号
-三个给予字符串创建新串:
slice():切片             // 第一个参数为起始位置,第二个参数为结束位置substr():子函数       // 第一个参数为起始位置,第二个参数为截取的个数substring():子串     // 第一个参数为起始位置,第二个参数为结束位置
以上方法都不会影响原始字符串
3)字符串位置方法:
indexof:从字符串中查找字符串,返回位置,查不到则返回-1.
indexOf:从头查找lastIndexOf:从尾查找  返回第一次出现的位置
4)trim:创建字符串副本,删除前后所有空格,原始字符串不变。
5)字符串大小写转变:
toLocalUpperCase():转大写,针对特定地区
toLocalLowerCase():转小写,针对特定地区
toUpperCase():转大写
toLowerCase():转小写
6)字符串模式匹配方法:
match():与RegExp的exec方法相同,参数为正则表达式活RegExp对象。
search():从尾开始查找,参数与match相同,找不到返回-1
replace():替换
split():切片,基于指定分隔符字符串为多个子字符串。第二个参数可选,返回几个数组。

7、单体内置对象

已自行实例化,可直接使用,Global和Math

* -Global对象,全局对象:所有全局作用域中属性和方法都是它的。
登录后复制

--url编码
--eval():解析字符串代码并执行

* -Math对象
登录后复制

--Math.ceil():向上取整。
--Math.floor():向下取整。
--Math.round():四舍五入。

* random()方法:随机数,返回0~1之间随机数。
登录后复制

本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的JavaScript视频教程栏目!

以上就是javaScript引用类型的详细介绍(附示例)的详细内容,更多请关注php中文网其它相关文章!

JavaScript的数据类型与变量的解析(附示例)

JavaScript的数据类型与变量的解析(附示例)

本篇文章给大家带来的内容是关于javascript的数据类型与变量的解析(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

这篇文章,来聊聊 JS 中的数据类型与变量。这是在学习 JS 时最基础的一类问题,但却很重要。希望我的分享有帮助到你。

文章开头,我先提几个面试中遇到的问题:

比如:

如何理解参数的按值传递?

立即学习“Java免费学习笔记(深入)”;

什么是暂时性死区?

什么是变量提升?

全局变量和 window 的属性有什么区别?为什么?

... ...

这篇文章的风格,在分析知识点的同时,插入一些我经历过的面试题。

基本数据类型

在 JS 中,基本数据类型有 6 种,即数值、字符串、布尔值、null、undefined、Symbol。

对于基本数据类型,我们需要明白的是:基本类型在内存中的存储方式是栈。每一个值都是单独存放,互不影响。

基本类型都是按值访问的。在比较时,按值进行比较:

1 === 1 // true
登录后复制

引用数据类型

引用类型的值保存在堆中,而引用是保存在栈中。

引用类型按引用访问。在比较时,也比较的引用:

{} === {} // =&gt; false
登录后复制

参数的传递方式

在 JS 中,参数可以是任何类型的值,甚至可以是函数。

这里要分析的是参数是以哪种类型传递的?引用类型还是基本类型?

先看一个基础的例子:

var out_num = 1;

function addOne(in_num) {
    in_num += 1;
    return in_num;
}

addOne(out_num); // =&gt; 2
out_num // =&gt; 1
登录后复制

这个例子中,我们给 addOne() 函数传递一个实参 out_num,这个时 out_num 会传递给 in_num,即内部存在着 in_num = out_num 的过程。最后我们看到的结果是 out_num 并没有被函数改变,说明 in_num 和 out_num 是两个在内存中独立存放的值,即按值传递。

再来看一个变形:

var out_obj = { value: 1 };

function addOne(in_obj) {
    in_obj.value += 1;
    return in_obj;
}

addOne(out_obj); // =&gt; { value: 2 }
out_obj // =&gt; { value: 2 }
登录后复制

问题来了?函数参数不是按值传递吗?为什么这里函数内部的处理反映到外部了?这是一个超级超级超级的理解误区。

首先,我们还是得摆正观点,即函数参数是按值传递的。那这里怎么理解呢?对于引用类型而言,前面说引用类型分为引用和实际的内存空间。在这里 out_obj 依旧传递给 in_obj,即 in_obj = out_obj ,out_obj 和 in_obj 是两个引用,它们在内存中的存储方式是独立的,但是它们却指向同一块内存。

而 in_obj.value = 1 则是直接操作的实际对象。实际对象的改变,会同步到所有引用这个实际对象的引用。

793519151-5bec30f841007_articlex.png

2727097527-5bec3115ca942_articlex.png

你再来看这个例子,或许就会更清晰一些。

var out_obj = { value: 1 };

function addOne(in_obj) {
    in_obj = { value: 2 };
    return in_obj;
}

addOne(out_obj); // =&gt; { value: 2 }
out_obj // =&gt; { value: 1 }
登录后复制

你只要抓住一点:对象的赋值就会造成引用指向的实际对象发生改变。

如何判断数据类型

判断数据类型,通常有三种具体的方法:

1、typeof 操作符

typeof 操作符返回一个表示数据类型的字符串。它存在以下明显的缺陷:

typeof null // =&gt; ''object''

typeof [] // =&gt; ''object''
登录后复制

这是因为在 JS 语言设计之初遗留的 bug。可以阅读这篇文章 http://2ality.com/2013/10/typ... 了解更多关于 typeof 处理 null 的问题。

所以 typeof 最好用于判断一些基本类型,比如数值、字符串、布尔值、undefined、Symbol。

2、instanceof 操作符

typeof  的背后是通过判断 type tags 来判断数据类型,而 instanceof 则是通过判断构造函数的 prototype 是否出现在对象原型链上的任何位置。

举个例子:

{} instanceof Object // =&gt; true

[] instanceof Array // =&gt; true
[] instanceof Object // =&gt; true
登录后复制

也判断自定义类型:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var auto = new Car(''Honda'', ''Accord'', 1998);

console.log(auto instanceof Car);
// =&gt; true

console.log(auto instanceof Object);
// =&gt; true
登录后复制

所以,对于字面量形式的基本数据类型,不能通过 instanceof 判断:

1 instanceof Number // =&gt; false

Number(1) instanceof Number // =&gt; false

new Number(1) instanceof Number // =&gt; true
登录后复制

3、Object.prototype.toString()

这是目前最为推荐的一种方法,可以更加精细且准确的判断任何数据类型,甚至是 JSON、正则、日期、错误等等。在 Lodash 中,其判断数据类型的核心也是 Object.prototype.toString() 方法。

Object.prototype.toString.call(JSON) // =&gt; "[object JSON]"
登录后复制

关于这背后的原理,你可以阅读这篇文章 http://www.cnblogs.com/ziyunf...

4、其他

上面三种是通用的判断数据类型的方法。面试中还会出现如何判断一个数组、如何判断 NaN、如何判断类数组对象、如何判断一个空对象等问题。这一类问题比较开放,解决思路通常是抓住判断数据的核心特点。

举个例子:判断类数组对象。

你先要知道 JS 中类数组对象是什么样子的,并寻求一个实际的参照物,比如 arguments 就是类数组对象。那么类数组对象具有的特点是:真值 & 对象 & 具有 length 属性 & length 为整数 & length 的范围大于等于 0,小于等于最大安全正整数(Number.MAX_SAFE_INTEGER)。

在你分析特点的时候,答案就呼之欲出了。【注意全面性】

数据类型如何转换

JS 数据类型的动态性将贯穿整个 JS 的学习,这是 JS 非常重要的特性,很多现象就是因为动态性的存在而成为 JS 独有。

正是由于动态性,JS 的数据类型可能在你毫无察觉的情况下,就发生了改变,直到运行时报错。

这里主要分析下面 8 种转换规则。

1、if 语句

if 语句中的类型转换是最常见的。

if (isTrue) {
    // ...
} else {}
登录后复制

在 if 语句中,会自动调用 Boolean() 转型函数对变量 isTrue 进行转换。

当 isTrue 的值是 null, undefined, 0, NaN, '''' 时,都会转为 false。其余值除 false 本身外都会转为 true。

2、Number() 转型函数

我们重点关注 null undefined 以及字符串在 Number() 下的转换:

Number(null) // =&gt; 0
Number(undefined) // =&gt; NaN
Number('''') // =&gt; 0
Number(''123'') // =&gt; 123
Number(''123abc'') // =&gt; NaN
登录后复制

注意和 parseInt() 对比。

3、parseInt()

parseInt(null) // =&gt; NaN
parseInt(undefined) // =&gt; NaN
parseInt('''') // =&gt; NaN
parseInt(''123'') // =&gt; 123
parseInt(''123abc'') // =&gt; 123
登录后复制

4、==

这里需要注意的是:

null == undefined // =&gt; true

null == 0 // =&gt; false
undefined == false // =&gt; false
登录后复制

null 与 undefined 的相等性是由 ECMA-262 规定的,并且 null 与 undefined 在比较相等性时不能转换为其他任何值。

5、关系操作符

对于两个字符串的比较,是比较的字符编码值:

''B''  true
登录后复制

一个数值,另一个其他类型,都将转为数字进行比较。

两个布尔值转为数值进行比较。

对象,先调用 valueOf(),若不存在该方法,则调用 toString()。

6、加法

加法中特别注意的是,数字和字符串相加,将数字转为字符串。

''1'' + 2 =&gt; // ''12''
1 + 2 =&gt; // 3
登录后复制

对于对象和布尔值,调用它们的 toString() 方法得到对应的字符串值,然后进行字符串相加。对于 undefined 和 null 调用 String() 取得字符串 ''undeifned'' 和 ''null''。

{ value: 1 } + true // =&gt; "[object Object]true"
登录后复制

7、减法

对于字符串、布尔值、null 或者 undefined,自动调用 Number(),转换结果若为 NaN,那么最终结果为 NaN。

对于对象,先调用 valueOf(),如果得到 NaN,结果为 NaN。如果没有 valueOf(),则调用 toString()。

8、乘法、除法

对于非数值,都会调用 Number() 转型函数。

变量提升与暂时性死区

JS 中有三种声明变量的方式:var, let, const。

var 声明变量最大的一个特点是存在变量提升。

console.log(a); // undefined
var a = 1;
console.log(a); // 1
登录后复制

第一个打印结果表示,在声明变量 a 之前,a 就已经可以访问了,只不过并未赋值。这就是变量提升现象。(具体原因,我放在后面分析作用域的时候来写)

let 和 const 就不存在这个问题,但是又引入了暂时性死区这样的概念。

/**
* 这上面都属于变量 a 的暂时性死区
* console.log(a) // =&gt; Reference Error
*/
let a = 1;
console.log(a); // =&gt; 1
登录后复制

即声明 a 之前,不能够访问 a,而直接报错。

而暂时性死区的出现又引出另外一个问题,即 typeof 不再安全。你可以参考这篇文章 http://es-discourse.com/t/why...

补充:一个经典面试题

for (var i = 0; i <p>我先不再这里展开分析,我打算放到异步与事件循环机制中去分析。不过这里将 var 替换成 let 可以作为一种解决方案。如果你有兴趣,也可以先去分析。</p><p>对于 const,这里再补充一点,用于加深对基本类型和引用类型的理解。</p><pre>const a = 1;
const b = { value: 1 };

a = 2; // =&gt; Error
b.value = 2; // =&gt; 2
b = { value: 2 }; // =&gt; Error
登录后复制

本质上,const 并不是保证变量的值不得改动,而是变量指向的内存地址不得改动。

声明全局变量

直接通过 var 声明全局变量,这个全局变量会作为 window 对象的一个属性。

var a = 1;
window.a // =&gt; 1
登录后复制

在这里提出两个问题,一是 let 声明的全局变量会成为 window 的属性吗?二是 var 声明的全局变量和直接在 window 创建属性有没有区别?

先来回答第一问题。let 声明的全局变量不会成为 window 的属性。用什么来支撑这样的结论呢?在 ES6 中,对于 let 和 const 声明的变量从一开始就形成封闭作用域。想想之前的暂时性死区。

第二个问题,var 声明的全局变量和直接在 window 创建属性存在着本质的区别。先看下面的代码:

var a = 1;
window.a // =&gt; 1

window.b = 2;

delete window.a
delete window.b

window.a // => 1
window.b // => undefined
登录后复制

我们可以看到,直接创建在 window 上的属性可以被 delete 删除,而 var 创建的全局属性则不会。这是现象,通过现象看本质,二者本质上的区别在于:

使用 var 声明的全局变量的 [[configurable]] 数据属性的值为 false,不能通过 delete 删除。而直接在对象上创建的属性默认 [[configurable]] 的值为 true,即可以被 delete 删除。(关于 [[configurable]] 属性,在后面的文章中分析对象的时候还会提到)

小结

在这篇「数据类型与变量」文章中,分析了 7 个大类。再来回顾一下:

基本类型、引用类型、参数传递方式、如何判断数据类型、数据类型如何转换、变量提升与暂时性死区、声明全局变量。

这些不仅是校招面试中的高频考点,也是学习 JS 必不可少的知识点。

Tip1:《JavaScript 高级程序设计》这本书被称作“前端的圣经”是有原因的。对于正在准备校园招聘的你,非常有必要!书读百遍,其义自见。你会发现你在面试中遇到的绝大部分 JS 相关的知识点都能在这本书中找到“答案”!

Tip2:在准备复习的过程中,注意知识的模块性与相关性。你得有自己划分知识模块的能力,比如今天的「数据类型与变量」模块。相关性是指,任何的知识都是由联系的,比如这里牵涉到作用域、内存等模块。

以上就是JavaScript的数据类型与变量的解析(附示例)的详细内容,更多请关注php中文网其它相关文章!

TypeScript 内置高级类型编程示例

TypeScript 内置高级类型编程示例

TypeScript 类型编程

TypeScript 的类型系统,最基本的是简单对应 JavaScript 的 基本类型,比如 string、number、boolean 等,然后是新增的 tuple、enum、复合类型、交叉类型、索引类型等 增强类型

这里会有一个问题,就是函数声明支持不同类型的重复编写问题,比如我的一个函数要接收一个数组,然后从中取中一个元素。

一旦我们传入的数组类型不同,都要写多一个 type 别名,未免太繁琐。

type getStrItem = (items: string[]) => string;
type getNumItem = (items: number[]) => number;
// ... 每增加一种类型都要写多了一个 type 别名
const getStrFirst: getStrItem = (a) => {
    return a[0];
}

为解决这个问题,TypeScript 引入了 泛型,让类型也能成为参数了。

type getItem<T> = (items: T[]) => T
const getStrFirst: getItem<string> = (a) => {
    return a[0];
}

上面的 T 就是一个类型参数,当我们通过 类型别名<具体类型> 形式(上面代码对应 getItem<string>),我们就能得到一个具体的类型了。

鉴于 JavaScript 太灵活,TypeScript 实现的是结构类型系统,我们又觉得泛型的简单推到 T 的粒度还是不够细,我们希望能够获取 T 内部的结构。

于是,TypeScript 在泛型的基础上,又提供了 类型编程,通过一些语法,我们可以拿到 T 下更细粒度的类型,或通过判断拿到其他类型。

这个也被大家戏称为 类型体操。可能是因为实现起来花里胡哨像是在参加体操大赛的原因。

总结一下,从类型能力上的增强的过程来说,就是:

基本类型 -> 泛型 -> 类型编程(类型体操)

TypeScript 内置高级类型

TS 代码版本为 4.8.2

下面我们来看一下 TypeScript 内置的几个高级类型,它们用了类型编程。

Pick<Type, Keys>

Pick 的作用是,从 T 类型(对象类型)中,提取出 K(联合类型)圈定的 key,返回一个新的对象类型。

这里我们通过 Pick 提取了需要的 pos 和 radius 物理信息属性。

看看 Pick 的实现:

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

首先我们看等号左侧的 <T, K extends keyof T>,类型参数有两个,T 和 K。

先说类型参数命名

类型变量命名和写 JS 变量一样,随意起名。但建议首字母大写,以防止和一些关键字混淆(比如 extends, as, infer),这些关键词都是小写的。

T 通常代表一个要被分析的类型(Type),K 通常代表对象属性名(Key)。就像数学中函数的 x 和 y 一样,想不到好的命名就用这俩。

keyof 是类型运算符,用于提取对象的属性(key),然后拼装成联合类型。

extends 用于限制类型参数的范围。比如 <T extends string> 表示 T 类型必须是 string 的子类,像字面量的 "a" 或 string 都是 string 的子类。如果不是 string 子类,编译无法通过。

还有一种是 extends ? : 的类似 JS 中三元运算符的语法,它在等号的右侧,用于实现条件判断。它和前面提到的 extends 不是同一样东西,后面我会说到。

Ok,我们整体看看 <T, K extends keyof T> 代表什么意思。它表示传入 T 和 K 两个类型参数,然后 K 必须是 T 的属性组成的联合类型中的一部分。

我们再看看等号右边 { [P in K]: T[P]; };,它是对类型进行 重映射

in 用于对联合类型进行遍历。也就是遍历我们需要用到的 key,作为索引 P,然后它的值还是用对应的 T[P]。

Exclude<UnionType, ExcludedMembers>

Exclude 的作用是,从联合类型中剔除掉一些类型。

实现如下:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

这里涉及到一个经常用到的 条件语法extends ? :,你可以把它类比为 JS 中的三元表达式(即 condition ? a : b)。

为了更好的讲解,我们实现一个类型 IsNumber,判断一个类型是否为数值类型。

type IsNumber<T> = T extends number ? true : false;
// 使用
type A = IsNumber<1> // true
type B = IsNumber<"str"> // false

T extends number 判断 T 是否为 number 的子类,如果是的话,返回 true,否则返回 false。

需注意和前面的类型参数上 extends 是完全不同的东西。

回到我们的 Exclude,逻辑就很清楚了,就是判断 T 是否为 U 的子类,如果是的话,返回 never(效果是被丢弃);否则返回 T。

你是不是有点奇怪结果,逻辑看起来不应该是 "a" | "b" | "c" 不是 "b" 的子类,返回 "a" | "b" | "c" 吗?怎么编程了 "a" | "c"?

其实这是联合类型的特殊逻辑,如果联合类型使用了 extends,它就会被打散,变成多个独立的类型进行判断,最后再组合起来

所以真正逻辑是, "a" | "b" | "c" 被打散,变成依次判断 "a" 、"b"、"c" 是否为 "b" 的子类,分别得到  "a" 、never、"c",然后联合起来,就变成了  "a" | "c"。

ReturnType<Type>

获取函数类型的返回值类型。

实现为:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

等号左侧的 (...args: any) => any 代表一个任意函数类型,用于限制传入参数的类型。

然后我们看到了一个新的关键词 infer,代表引用的意思,用于类型推导。

extends 和 infer 搭配,可以实现 模式匹配,如果 extends 匹配成功,infer 就能推导获得对应的类型。

如果你了解  JS 的正则表达式,你会发现它们很像,infer 好比是捕获组。

''ABC''.replace(/A(.)C/, ''$1'') 
// ''B''。提取了模式上匹配的一个字符串

T extends (...args: any) => infer R ? R : any; 中,我们给返回值部分设置了 infer,并提供了一个局部变量 R。

如果 extends 条件判断是继承关系,那么变量 R 就会被赋值函数的返回值。

后面的判断为真的分支(? 后面的表达式)就能拿到这个 R。判断为假的分支就无法拿到,因为匹配失败了。

这个 extends + infer 其实就是类型体操的精髓,可以在传入类型 T 继续拆分,拿到更细粒度的类型。

更多类型体操学习

还有更多的类型编程的技巧因为篇幅原因就不说了,比如还有:

  • as 运算符可以做类型索引的重映射;
  • 通过数组的 "length" 可以实现数字运算;
  • 通过递归实现循环逻辑;
  • 一些特殊的类型(比如 never)的处理等。

TypeScript 的类型是图灵完备的,可以实现各种判断、循环、加减的逻辑。当然某些逻辑实现起来很繁琐就是了。

它的语法也是与众不同:它做了 “压缩”。一个类型的编程只是一个表达式,需要用 extend ? : 的方式不停嵌套实现逻辑。TS 类型体操学起来,某种意义上确实有点像学一门新的语言,而且有那么一点古怪。

以上就是TypeScript 内置高级类型编程示例的详细内容,更多关于TypeScript 内置类型的资料请关注其它相关文章!

您可能感兴趣的文章:
  • typescript中高级类型Record详解
  • 你需要知道的TypeScript高级类型总结
  • 你不知道的 TypeScript 高级类型(小结)
  • Typescript高级类型Record,Partial,Readonly详解

TypeScript 高级类型

TypeScript 高级类型

⒈交叉类型(Intersection Types)

  交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

  每当我们正确的使用交叉类型的时候,TypeScript可以帮我们合理地将两个不同类型叠加为新的类型,并包含了所需的所有类型。

  我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!)

type newType = number & string;
let a : newType;


interface A{
  a:number,
  b:string,
}
interface B{
  c:string,
  d:string,
}
type newType2 = A & B;
let b : newType2;

  这里的Type关键字是用来声明类型变量的。在运行时,与类型相关的代码都会被移除掉,并不会影响到JavaScript的执行。

  *当交叉类型中有属性冲突时,则无论如何赋值都不可能通过类型检查。如下面的代码所示:

interface A{
  a:number,
  b:string,
}
interface B{
  c:string,
  d:string,
}
type newType = A & B;
let a : newType = {a:1,b:'''',c:'''',d:''''};
a.a = 1;
a.b = '''';
a.c = '''';
a.d = 5;  //Error,无法通过类型检查

 

⒉联合类型(Union Types)

  联合类型与交叉类型类似,但使用上却完全不同。

  例如我们需要一个变量可能是number,也有可能是string,这是一个很常见的场景,联合类型便是用于解决这样的问题。

  比如下面这段经典的函数:

function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got ''${padding}''.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

  padLeft函数存在一个问题, padding参数的类型指定为 any。 也就是说,我们可以传入一个既不是 number也不是 string类型的参数,但是TypeScript却不报错。

let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错

  在传统的面向对象语言里,我们可以使用重载或将这两种类型抽象成有层级的类型(父类与子类)。 这么做显然是非常清晰的,但同时也存在了过度设计。

  因为在JavaScript中并没有重载可以使用(可以使用特殊的方式创建出类似重载的函数),因此在JavaScript的函数中手动去判断参数的类型这种操作更为常见,这在一定程度上避免了过度设计。

   padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。

  如果我们希望更准确的描述padding的类型,就可以使用联合类型将padding的类型限定为既可以是number又可以是string。

  代替 any, 我们可以使用 联合类型作为 padding的参数:

function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft("Hello world", true); // 编译器报错,类型true的参数不能赋值给类型string|number的参数

  联合类型表示一个变量可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 numberstring或 boolean

  注意,如果一个值是联合类型,我们只能访问它们共有的属性或方法。

  我们来看一下下面的例子:

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

  如果一个值的类型是 A | B,我们能够 确定的是它包含了 A  B中共有的成员。 这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 pet.fly()就出错了。

  联合类型取的是交集,交叉类型取的是并集,这听起来和它们的名字有些冲突。

  **谨记,TypeScript只会帮你在编译时做类型检查,并不确保你的代码在运行过程中的安全。

⒊类型保护【区分值的类型】

  联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解某个值的类型时该怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。

let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

  而在TypeScript中,我们可以使用类型断言。

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

  为了准确判断值的类型,我们在方法体中多次使用了类型断言(即使通过了类型断言,我们知道了值的类型,在接下来的代码中,我们仍然要对其添加类型断言),这是一件非常麻烦的事情。如果我们一旦检查并确定了值的类型,在之后的代码中无需类型断言就能清楚地知道值的类型的话就好了。

  1.自定义类型保护(用户自定义的类型保护)

  TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 既使可读性得到提升,又减少了使用烦琐的类型断言。要定义一个类型保护,我们只需要简单地定义一个函数就可以,但返回值是一个主谓宾语句( 类型谓词),如下所示:

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

  在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

  每当使用一些变量调用 isFish时,TypeScript会将变量指定为类型保护中的类型,只要这个类型与变量的原始类型是兼容的。

// ''swim'' 和 ''fly'' 调用都没有问题了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

  注意,TypeScript不仅知道在 if分支里 pet是 Fish类型; 它还清楚在 else分支里pet一定是 Bird类型,这得益于类型保护的实现。

 

  2.typeof类型保护

  现在我们可以使用类型保护来重构一开始的padLeft代码了,可以考虑用联合类型书写 padLeft代码。 可以像下面这样利用类型断言来写:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got ''${padding}''.`);
}

  然而,每次typeof进行类型判断都必须要定义一个函数,这太痛苦了。 幸运的是,现在我们不必将 typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got ''${padding}''.`);
}

  typeof类型保护只有两种形式能被识别: typeof v === "typename"和 typeof v !== "typename",且typeof在TypeScript中使用时,只有匹配基本类型时(即"typename"必须是 "number", "string", "boolean"或 "symbol"),才会启动类型保护 。 但是TypeScript并不会阻止你与其它字符串比较(例如typeof v === ''hello TypeScript''),typeof并不会把它识别为一个有效的类型,因此也不会把这些字符串识别为类型保护。

  

  3.instanceof类型保护

  除了typeof以外,instanceof也可以起来类型保护的作用。Instanceof相较于typeof,其类型保护更为精细,是通过构造函数来区分类型的一种方式。

  如果你已经阅读了 typeof类型保护并且对JavaScript里的 instanceof操作符熟悉的话,你可能已经很轻而易举的在TypeScript中使用instanceof类型保护了。

  instanceof类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为''SpaceRepeatingPadder''
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为''StringPadder''
}

  可以看出instanceof在类型的使用上,与typeof相比,可以将类作为比较对象,从而实现类型保护。

  instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  1. 此构造函数的 prototype属性的类型,如果它的类型不为 any的话
  2. 构造签名所返回的类型的联合

  以此顺序。

⒋可以为null的类型

  TypeScript具有两种特殊的类型, null和 undefined,它们分别具有值null和undefined. 我们在[基础类型](./Basic Types.md)一节里已经做过简要说明。 默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型。 null与 undefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为 价值亿万美金的错误。

  --strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们

let s = "foo";
s = null; // 错误, ''null''不能赋值给''string''
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, ''undefined''不能赋值给''string | null''

  注意,按照JavaScript的语义,TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。

 

⒌可选参数和可选属性

  使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, ''null'' is not assignable to ''number | undefined''

  可选属性也会有同样的处理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, ''undefined'' is not assignable to ''number''
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, ''null'' is not assignable to ''number | undefined''

 

⒍类型保护和类型断言

  由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。 幸运地是这与在JavaScript里写的代码一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

  这里很明显地去除了 null,你也可以使用短路运算符:

function f(sn: string | null): string {
    return sn || "default";
}

  如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + ''.  the '' + epithet; // error, ''name'' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + ''.  the '' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

  本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name的类型。

 

⒎类型别名

  类型别名就是可以给一个类型起个新名字。类型别名有时和接口很像, 类型别名可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

  如果你学过C语言,可能还记得alias关键字,不过在TypeScript中,我们使用type关键字来描述类型变量。 

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === ''string'') {
        return n;
    }
    else {
        return n();
    }
}

  使用别名并不会在类型系统中新建一个类型 - 它创建了一个新的名字引用那个类型。 给基本类型起别名通常没什么用,一般是用来减少文档的编写量。

  类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

type Person<T> = {age : T};

  也可以使用类型别名在属性里引用自己,这看起来很像是递归。 

type Person<T> = {
    name : T;
    mother : Person<T>;
    father : Person<T>;
}

  这使得类型编排非常复杂。当然,这种复杂性是为了描述的准确性,正如上面的例子,mother和father肯定也是person。这样在代码中看上去有点不可思议的操作,在现实世界中却是非常真实合理的。

  与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

  然而,类型别名不能出现在声明右侧的任何地方。

type Yikes = Array<Yikes>; // error

  接口 vs. 类型别名

  像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。

  其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

  另一个重要区别是类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。

  另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

 

⒏字符串字面量类型

  我们先看一个简单的字面量类型,比如下面这个字符串常量。

type Profession = "teacher";

  字符串字面量类型允许你指定字符串必须的固定值。

  在实际应用中,通常字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 而通过结合使用这些特性,达到类似枚举类型的效果。

type Profession = "teacher" | "doctor" | "accountant";
function personCreator(Profession : Profession){
  //省略函数内部的具体实现,这并不影响案例的运行
}

personCreator("teacher");
personCreator("doctor");
personCreator("accountant");

personCreator("programmer");  //Error

  你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。【参见上面的联合类型】

  字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

  

⒐数字字面量类型

  TypeScript还具有数字字面量类型,其用法和字符串字面量一致。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

  我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator ''!=='' cannot be applied to types ''1'' and ''2''.
    }
}

  换句话说,当 x与 2进行比较的时候,它的值必须为 1,这就意味着上面的比较检查是非法的。

 

⒑枚举成员类型

  如我们在 枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

  在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。

  可辨识联合(Discriminated Unions)

  你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

  1. 具有普通的单例类型属性— 可辨识的特征
  2. 一个类型别名包含了那些类型的联合— 联合
  3. 此属性上的类型保护。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

  首先我们声明了将要联合的接口。 每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:

type Shape = Square | Rectangle | Circle;

  现在我们使用可辨识联合:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

  完整性检查

  当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle到 Shape,我们同时还需要更新 area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn''t handle case "triangle"
}

  有两种方式可以实现。 首先是启用 --strictNullChecks并且指定一个返回值类型:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

  因为 switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined。 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined。 然而,这种方法存在些微妙之处且 --strictNullChecks对旧代码支持不好。

  第二种方法使用 never类型,编译器用它来进行完整性检查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

  这里, assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

  多态的 this类型

  多态的 this类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回 this类型:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

  由于这个类使用了 this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

  如果没有 this类型, ScientificCalculator就不能够在继承 BasicCalculator的同时还保持接口的连贯性。 multiply将会返回 BasicCalculator,它并没有 sin方法。 然而,使用 this类型, multiply会返回 this,在这里就是 ScientificCalculator

  索引类型(Index types)

  使用索引类型,编译器就能够检查使用了动态属性名的代码。例如,一个常见的JavaScript模式是从对象中选取属性的子集。

function pluck(o, names) {
    return names.map(n => o[n]);
}

  在TypeScript里通过 索引类型查询和 索引访问操作符使用此函数:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: ''Jarid'',
    age: 35
};
let strings: string[] = pluck(person, [''name'']); // ok, string[]

  编译器会检查传入的值是否是 Person的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T, 索引类型查询操作符。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。 例如:

let personProps: keyof Person; // ''name'' | ''age''

  keyof Person是完全可以与 ''name'' | ''age''互相替换的。 不同的是如果你为Person添加了新的属性 ,例如 address: string,那么 keyof Person会自动变为 ''name'' | ''age'' | ''address''。 你可以在像 pluck函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck

pluck(person, [''age'', ''unknown'']); // error, ''unknown'' is not in ''name'' | ''age''

  第二个操作符是 T[K], 索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 person[''name'']具有类型 Person[''name''] — 在我们的例子里则为 string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K],这正是它的强大所在。 你只要确保类型变量 K extends keyof T就可以了。 例如下面 getProperty函数的例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

  getProperty里的 o: T和 name: K,意味着 o[name]: T[K]。 当你返回 T[K]的结果,编译器会实例化键的真实类型,因此 getProperty的返回值类型会随着你需要的属性改变。

let name: string = getProperty(person, ''name'');
let age: number = getProperty(person, ''age'');
let unknown = getProperty(person, ''unknown''); // error, ''unknown'' is not in ''name'' | ''age''

  索引类型和字符串索引签名

  keyof和 T[K]与字符串索引签名进行交互。 如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>[''foo'']; // number

  让我们解释下上面一开始代码的意义。首先看泛型,这里有T和K两种类型。根据类型推断,第一个参数o就是person,类型会被推断为Person,而第二个数组参数的类型推断,我们可以从右往左进行阅读,keyof关键字可以获取T(此处为Person)的所有属性名,即[''name'',''age''],泛型K通过extends关键字继承了T(此处为Person)的所有属性名,即[''name'',''age'']。

  依托于keyof关键字完成了类型索引。

  我们再来看返回值,返回值的类型是T[K][],阅读起来有些困难,它实际上表述的意思是,变量T取属性K的值的数组,其中T[K]就是索引访问操作符。

  这样强大的功能保证了代码的动态性和准确性,也让代码提示变得更加丰富了。

  映射类型

  一种常见的场景是将一个已知类型的每个属性都变为可选的,这样在实例化该类型时就不必为每个类型都赋值了。

interface Person {
    name?: string;
    age?: number;
}

  或者是我们想要一个该类型的只读版本【即该类型的属性值都是只读不可修改的】

interface Person {
    readonly name: string;
    readonly age: number;
}

  这在JavaScript里会经常用到,而TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为只读类型或可选类型。 下面是一些例子:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

  像下面这样使用:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

  下面来看看最简单的映射类型和它的组成部分:

type Keys = ''option1'' | ''option2'';
type Flags = { [K in Keys]: boolean };

  它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  1. 类型变量 K,它会依次绑定到每个属性。
  2. 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

  在个简单的例子里, Keys是硬编码的的属性名列表并且属性类型永远是 boolean,因此这个映射类型等同于:

type Flags = {
    option1: boolean;
    option2: boolean;
}

  在真正的应用里,可能不同于上面的 Readonly或 Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof和索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

  但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

  在这些例子里,属性列表是 keyof T且结果类型是 T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是 同态的,映射只作用于 T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name是只读的,那么 Partial<Person>.name也将是只读的且为可选的。

  下面是另一个例子, T[P]被包装在 Proxy<T>类里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

  注意 Readonly<T>和 Partial<T>用处不小,因此它们与 Pick和 Record一同被包含进了TypeScript的标准库里:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

  Readonly, Partial和 Pick是同态的,但 Record不是。 因为 Record并不需要输入类型来拷贝属性,所以它不属于同态:

type ThreeStringProps = Record<''prop1'' | ''prop2'' | ''prop3'', string>

  非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

  TypeScript中内置了Readonly和Partial,所以不需要手动声明实现。

  内置的类型还有Required、Pick、Record、Exclude、Extract、NonNullable;它们的实现都在typescript/lib/lib.es5.d.ts中。

  由映射类型进行推断

  现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

  注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

  预定义的有条件类型

  TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

  示例

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

  注意:Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。我们没有增加Omit<T, K>类型,因为它可以很容易的用Pick<T, Exclude<keyof T, K>>来表示。

 

今天关于TypeScript高级类型备忘录附示例的讲解已经结束,谢谢您的阅读,如果想了解更多关于javaScript引用类型的详细介绍(附示例)、JavaScript的数据类型与变量的解析(附示例)、TypeScript 内置高级类型编程示例、TypeScript 高级类型的相关知识,请在本站搜索。

本文标签:

上一篇Spring IOC源码笔记-SpringBean的实例化(spring的ioc源码解析)

下一篇如何设计一个好用的 React Image 组件?(react组件设计原则)