类型兼容
如果,我们定义一个包含属性name的对象要:
interface Name {
name : string
}
let obj : Name;
这样我们定义obj的时候就只能并且必须使obj有一个name属性。假如,在赋值时多加了一个height属性:
interface Name {
name : string
}
let obj : Name;
obj = {
name : 'cxh',
height : 180
}
则typescript会报错
所以,对obj的赋值必须是
obj = {
name : 'cxh',
}
但是,如果,我们只是定义了这种类型,并没有赋值给obj,不过在以后的运算里难免会有重新对obj赋值,可是我们不能保证对obj赋值的对象是不是符合Name,怎么办?其实typescipt自带类型兼容,它会检查赋值的变量(比如dabao)是否包含name属性,如果包含name则obj兼容dabao.例如:
interface Name {
name : string
}
let obj : Name;
let dabao = {
name : 'cxh',
height : 180
}
obj = dabao;
虽然,dabao是有height属性,但是因为dabao含有name属性,所以根据TypeScript结构化类型系统的基本规则,obj兼容dabao。
在进行赋值前,编译器会对dabao的属性进行遍历,查看是否含有name,并且name是否是string。符合情况就会赋值。
如果要是反过来呢?
interface Name {
name : string;
age : number
}
let obj : Name;
let dabao = {
name : 'cxh'
}
obj = dabao;
obj需要 name ,age两个属性,但是dabao只有一个 name,编译器遍历dabao,发现并没有找到age,所以肯定报错了
函数兼容
函数的兼容和普通类型或者对象是有差异的。下面我们从函数的参数,以及返回类型进行分析:
let dabao : { (name:string, age : number ,height : number) : object }
let xiaobao : {(name : string, height:number) : object}
xiaobao = function (name : string, height : number) {
return {
name : name
}
}
dabao = xiaobao
对于函数的参数,和对象是有差别的,编译器会遍历xiaobao的参数,如果xiaobao的每个参数类型,能在dabao里面找到,并且顺序一致,就可以赋值。( 注意的是参数的名字相同与否无所谓,只看它们的类型。)差距在于:
对于对象: 要检查 赋值者 包含 被赋值者。
对于函数参数:要检查 赋值者 被 被赋值者 包含。
如果顺序不一致也会报错。
返回值检查
let dabao : { (name:string, height : number) : {name : string} }
let xiaobao : {(name : string, height:number) : {name : string; height:number}}
xiaobao = function (name:string ,height:number) {
return {
name : name,
height : height
}
}
dabao = xiaobao
编译器对xiaobao的返回值进行检查,如果xiaobao的返回值包含dabao的返回值,则可以复制。
函数的返回值和对象一样。
其实这很好理解,如果我们定义了一个对象必须要用到name属性,在赋值的时候即使它多出了很多属性,也无所谓,大不了我们不用。但是少了我们需要的属性,那就用不了了。
但是函数的参数刚好相反,函数是可以少传或者不传参数,但是多传就会报错。
类
class dabao {
name : string
age : number
constructor(name: string,age: number){
}
}
class xiaobao {
name : string
constructor(name : boolean){
}
}
let d : dabao;
let x : xiaobao
x = d
d如果包含x的属性,就可以赋值。类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class dabao {
name : string
age : number
constructor(name: string,age: number){
}
}
class xiaobao {
name : string
static height : number
constructor(name : boolean){
}
}
let d : dabao;
let x : xiaobao
x = d
私有成员
私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
class dabao{
name : string
age : number
private height : number
constructor(name: string,age: number){
}
}
class xiaobao{
name : string
constructor(name : boolean){
}
}
let d : dabao;
let x : xiaobao
x = d
如果这样:
class dabao {
name : string
age : number
private height : number
constructor(name: string,age: number){
}
}
class xiaobao {
name : string
private height : number
constructor(name : boolean){
}
}
let d : dabao;
let x : xiaobao
x = d
这样是不可以的,不过如果是protected我们可以这样解决
class obj {
protected height : number
}
class dabao extends obj{
name : string
age : number
constructor(name: string,age: number){
super()
}
}
class xiaobao extends obj{
name : string
constructor(name : boolean){
super()
}
}
let d : dabao;
let x : xiaobao
x = d
泛型
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。
let dabao = function<T>(x: T): T {
return x
}
let xiaobao = function<U>(y: U): U {
return y
}
dabao = xiaobao;
这样也是可以的
let dabao = function<T>(x: T): T {
return x
}
let xiaobao = function(y:string){
return y
}
dabao = xiaobao;
但是如果这样
interface dabao<T> {
name : T
}
let a : dabao<string>
let b : dabao<number>
a = b
就会报错 因为在b里面虽然有name 但是类型不一样。所以编译器会报错。
高级类型
交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。例如:
interface dabao {
name : string;
age : number;
}
interface xiaobao {
age : number;
height : number;
}
let func : <T,U>(name:T,age:U) => T = function<T,U>(name:T,age:U) : T&U{
return Object.assign(name,age)
}
console.log(func<dabao,xiaobao>({name:'cxh',age:12},{age:123,height:180}))
结果:
联合类型
有下面一段代码
function dabao(name: string, age : any) {
if (typeof age == 'string') {
return age + 1;
}else if(typeof age == 'number') {
return age;
}
}
假如我们调用dabao('cxh',true);age既不是string和number类型,而且typescript不会报错。typescript提供了联合类型
function dabao(name: string, age : string | number) {
if (typeof age == 'string') {
return age + 1;
}else if(typeof age == 'number') {
return age;
}
}
interface dabao {
name : string;
width:number;
}
interface xiaobao {
name : string;
age: number;
height: number;
}
let d : dabao | xiaobao;
d = {
name : 'cxh',
age : 123,
height : 123,
width : 123
}
从上面这段代码我们可以看见实际上是没有报错的,d既可以是dabao类型,也可以是xiaobao类型,也可以是dabao加上xiaobao如上所示。但是:
当我们调用d的属性时,发现只有name可以用。
由此我们可以得出交叉类型在定义的时候必须要结合两者的属性:
interface dabao {
name : string;
width:number;
set(): void;
}
interface xiaobao {
name : string;
age: number;
height: number;
}
let d : dabao & xiaobao;
d = {
name : 'cxh',
width : 12,
set : () => {},
age : 12,
height: 180
}
console.log(d.name)
console.log(d.age)
console.log(d.height)
console.log(d.width)
即使少一个属性也会报错:
结合类型在定义的时候至少包含其中一种类型:
interface dabao {
name : string;
width:number;
set(): void;
}
interface xiaobao {
name : string;
age: number;
height: number;
}
let d : dabao | xiaobao;
d = {
name : 'cxh',
width : 12,
set : () => {},
}
或者
d = {
name : 'cxh',
width : 12,
set : () => {},
age: 13
}
再或者
d = {
name : 'cxh',
width : 12,
set : () => {},
age: 13,
height: 180
}
都不会报错,但是无论上述哪种能生效的只有共有的属性;
typescript在定义类型的时候d既可以是dabao类型也是xiaobao类型,但是typescript并不能确定用户到底要传哪一个,所以对于这两种类型单独存在的成员不能保证到底能不能有。所以索性只去识别公共的部分。
但是,如果我们非得需要去识别呢?
第一种方式就是使用类型断言
console.log((<dabao>d).name)
console.log((<dabao>d).width)
console.log((<xiaobao>d).age)
console.log((<xiaobao>d).height)
第二种:用户自定义的类型保护
用户自定义的类型保护
function isDabao(d : dabao | xiaobao) : d is dabao{
return (<dabao>d).width !== undefined;
}
调用函数isDabao 检查d是否为dabao 是即返回true。
interface dabao {
name : string;
width:number;
set(): void;
}
interface xiaobao {
name : string;
age: number;
height: number;
}
let d : dabao | xiaobao;
d = {
name : 'cxh',
width : 12,
set : () => {},
age: 13,
height: 180
}
function isDabao(d : dabao | xiaobao) : d is dabao{
return (<dabao>d).width !== undefined;
}
if(isDabao(d)){
console.log(d.width)
}else {
console.log(d.height)
}
typeof类型保护
function isNumber(x : any) :x is number{
return typeof x === 'number'
}
instanceof类型保护
class dabao {
name : string = 'cxh';
height : number = 180
}
class xiaobao {
name : string = 'yy'
age : number = 26
}
let d : dabao | xiaobao;
d = new dabao;
if (d instanceof dabao) {
console.log(d.height) ;
}
if (d instanceof xiaobao) {
console.log(d.age) ;
}
或者
interface obj {
timer : number
}
class dabao implements obj{
timer : number
name : string = 'cxh';
height : number = 180
}
class xiaobao implements obj{
timer : number
name : string = 'yy'
age : number = 26
}
let d : obj;
function getRandom() {
return Math.random() < 0.5 ?
new dabao() :
new xiaobao();
}
d = getRandom();
if (d instanceof dabao) {
console.log(d.height) ;
}
if (d instanceof xiaobao) {
console.log(d.age) ;
}
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
let a = getName('cxh');
let b = getName(() => {
return 'dabao'
})
console.log(a)
console.log(b)
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
我们也可以使用类型别名来在属性里引用自己:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
let t : Tree<string>
t.value = 'cxh';
t.left.value = 'ss'
t.left.left.value = 'ss'
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
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;
接口 vs. 类型别名
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是Interface,但悬停在aliased上时,显示的却是对象字面量类型。
另一个重要区别是类型别名不能被extends和implements(自己也不能extends和implements其它类型)。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
}else if (easing === "ease-out") {
}else {
}
}
可辨识联合(Discriminated Unions)
你可以合并字符串字面量类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
具有普通的字符串字面量属性—可辨识的特征。
一个类型别名包含了那些类型的联合—联合。
此属性上的类型保护。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
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的时候也更加明显。