1. Union Types
Intersection Types
Combine multiple types to produce new type, source types have "and" relationship, for example:
interface ObjectConstructor {
assign<T, U>(target: T, source: U): T & U;
}
(From TypeScript/lib/lib.es2015.core.d.ts)
Object.assign can shallow copy enumerable properties from source: U to target: T, therefore return value type is T & U
Intersection type A & B is both A and B, therefore has all members of each source type:
interface A {
a: string;
}
interface B {
b: number
}
let x: A & B;
// All are legal
x.a;
x.b;
P.S. Although named intersection, actually it's "finding union"
Union Types
Similar to intersection types, union types are composed of multiple types with "or" relationship, for example:
interface DateConstructor {
new (value: number | string | Date): Date;
}
(From TypeScript/lib/lib.es2015.core.d.ts)
Date constructor accepts a number or string or Date type parameter, corresponding type is number | string | Date
Union type A | B is either A or B, therefore only common members ("intersection") of all source types can be accessed:
interface A {
id: 'a';
a: string;
}
interface B {
id: 'b';
b: number
}
let x: A | B;
x.id;
// Error Property 'a' does not exist on type 'A | B'.
x.a;
// Error Property 'b' does not exist on type 'A | B'.
x.b;
2. Type Guards
Union types are equivalent to enum types composed of types, therefore cannot determine its specific type:
Union type
A | Bis eitherAorB
This is no problem in function signatures, but in function implementation, usually need to distinguish specific type, for example:
let createDate: (value: number | string | Date) => Date;
createDate = function(value) {
let date: Date;
if (typeof value === 'string') {
value = value.replace(/-/g, '/');
// ...
}
else if (typeof value === 'number') {/*...*/}
else if (value instanceof Date) {/*...*/}
return date;
};
Therefore, in such scenarios, need to "narrow" the "wide" union type to a specific type. From type perspective, above code should be like this in ideal case:
function(value) {
// Here, value is union type, either number or string or Date
if (typeof value === 'string') {
// In this branch, value is string
}
else if (typeof value === 'number') {
// In this branch, value is number
}
else if (value instanceof Date) {
// In this branch, value is Date
}
// Here, value is union type, either number or string or Date
}
That is, need a mechanism to let us tell type system, "listen, now I know the specific type of this thing, please narrow it down"
And this mechanism, is type guard
A type guard is some expression that performs a runtime check that guarantees the type in some scope.
typeof Type Guard
typeof variable === 'type' is idiomatic way to determine basic types, therefore TypeScript can recognize typeof, and automatically narrow union type in corresponding branch:
let x: number | string;
if (typeof x === 'string') {
// Correct typeof type guard, automatically narrows to string
x.toUpperCase();
}
Also applicable in switch statements, && and other branch structures:
switch (typeof x) {
case 'number':
// Correct typeof type guard
x.toFixed();
break;
}
// Correct typeof type guard
typeof x !== 'number' && x.startsWith('xxx');
Note, last example is quite interesting, x is either number or string, from typeof judgment known not number, therefore narrows to string
Specifically, typeof type guard can recognize two forms of typeof:
-
typeof v === "typename" -
typeof v !== "typename"
And typename can only be number, string, boolean or symbol, because remaining typeof detection results are not that reliable (specifically see typeof), so not used as type guard, for example:
let x: any;
if (typeof x === 'function') {
// any type, typeof type guard not applicable
x;
}
if (typeof x === 'object') {
// any type, typeof type guard not applicable
x;
}
P.S. Related discussion, see typeof a === "object" does not type the object as Object
instanceof Type Guard
Similar to typeof detecting basic types, instanceof is used to detect instance and "class" ownership relationship, also a type guard, for example:
let x: Date | RegExp;
if (x instanceof RegExp) {
// Correct instanceof type guard, automatically narrows to RegExp instance type
x.test('');
}
else {
// Correct automatically narrows to Date instance type
x.getTime();
}
Specifically, requires instanceof right side is a constructor, at this point left side type will be narrowed to:
-
Type of that class instance (constructor
prototypeproperty's type) -
(When constructor has overloaded versions) Union type composed of constructor return types
For example:
// Case1 Type of that class instance
let x;
if (x instanceof Date) {
// x narrows from any to Date
x.getTime();
}
// Case2 Union type composed of constructor return types
interface DateOrRegExp {
new(): Date;
new(value?: string): RegExp;
}
let A: DateOrRegExp;
let y;
if (y instanceof A) {
// y narrows from any to RegExp | Date
y;
}
P.S. For more information about instanceof type guard, see 4.24 Type Guards
P.S. Additionally, [class has dual type meanings](/articles/类-typescript 笔记 4/#articleHeader8), manifestation form in TypeScript code is as follows:
-
Class type:
typeof className -
Class instance type:
typeof className.prototypeorclassName
For example:
class A {
static prop = 'prop';
id: 'b'
}
// Class type
let x: typeof A;
x.prop;
// Error id is instance property, doesn't exist on class
x.id;
// Class instance type
let y: typeof A.prototype;
let z: A;
// Both types are equivalent
z = y;
// Error prop is static property, doesn't exist on instance
z.prop;
z.id;
That is, class instance type is equivalent to constructor prototype property's type. But this only holds at TypeScript compile time, conflicts with JavaScript runtime concept:
class A {}
class B extends A {}
// Constructor prototype property is parent class instance, its type is parent class instance type
B.prototype instanceof A === true
Custom Type Guards
typeof and instanceof type guards can satisfy general scenarios, for some more special cases, can narrow types through custom type guards:
interface RequestOptions {
url: string;
onSuccess?: () => void;
onFailure?: () => void;
}
// Custom type guard, narrows parameter type any to RequestOptions
function isValidRequestOptions(opts: any): opts is RequestOptions {
return opts && opts.url;
}
let opts;
if (isValidRequestOptions(opts)) {
// opts narrows from any to RequestOptions
opts.url;
}
Custom type guard is similar to normal function declaration, just return type part is a type predicate:
parameterName is Type
Among them parameterName must be parameter name in current function signature, for example above opts is RequestOptions
After calling function with type predicate, passed parameter's type will be narrowed to specified type, consistent with previous two type guard behaviors:
let isNumber: (value: any) => value is number;
let x: string | number;
if (isNumber(x)) {
// Narrows to number
x.toFixed(2);
}
else {
// Not number is string
x.toUpperCase();
}
3. Nullable and Union Types
In TypeScript empty types (Void) have two: Undefined and Null, are subtypes of (except Never) all other types. Therefore null and undefined can be assigned to any other type:
let x: string;
x = null;
x = undefined;
// Runtime error, compile time no error
x.toUpperCase();
From type perspective, Nullable type is equivalent to original type and null | undefined composed union type (in above example, equivalent to let x: string | null | undefined;)
This means type checking is not that very reliable, because still cannot avoid errors like undefined/null.xxx
strictNullChecks
For potential problems of empty types, TypeScript provides --strictNullChecks option, after enabling will strictly check empty types:
let x: string;
// Error Type 'null' is not assignable to type 'string'.
x = null;
// Error Type 'undefined' is not assignable to type 'string'.
x = undefined;
For types that can be empty, need to explicitly declare:
let y: string | undefined;
y = undefined;
// Type 'null' is not assignable to type 'string | undefined'.
y = null;
Meanwhile, optional parameters and optional properties will automatically carry | undefined, for example:
function createDate(value?: string) {
// Error Object is possibly 'undefined'.
value.toUpperCase();
}
interface Animal {
color: string;
name?: string;
}
let x: Animal;
// Error Type 'undefined' is not assignable to type 'string'.
x.color = undefined;
// Error Object is possibly 'undefined'.
x.name.toUpperCase();
Similar null value related problems can all be exposed, from this perspective, empty type strict checking is equivalent to a compile time checking ability to trace null values
! Suffix Type Assertion
Since Nullable type is essentially union type, similarly faces type narrowing problem. For this, TypeScript also provides intuitive type guard:
function createDate(value: string | undefined) {
// Narrows to string
value = value || 'today';
value.toUpperCase();
}
For scenarios automatic type guard cannot handle, can simply remove | undefined | null component through ! suffix:
function fixed(name: string | null): string {
function postfix(epithet: string) {
// Remove null component from type through !,使之 narrows to string
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
identifier! is equivalent to [type assertion](/articles/基本类型-typescript 笔记 2/#articleHeader4) (different from type guard):
let x: string | undefined | null;
x!.toUpperCase();
// Equivalent to
(<string>x).toUpperCase();
// Or
(x as string).toUpperCase();
// Object is possibly 'null' or 'undefined'.
x.toUpperCase();
P.S. Difference between type assertion and type guard is, assertion is one-time (or said temporary), while type guard is effective under certain scope
No comments yet. Be the first to share your thoughts.