.d.ts 文件只包含接口,不包含实现。npm 包安装后一般包含.d.ts文件和编译后的js文件,这里的js文件一般是bundled (多个源文件合并成一个主文件)
The declare
keyword: It only declares it for the TypeScript compiler, not for the JavaScript runtime.
Basic Types
Array types can be written in one of two ways. They are Arrays in JS with elements of the same type.
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
Tuples are the Arrays of fixed number of elements (of known types) in JS
// Declare a tuple type
let x: [string, number];
TS has an extra enumeration type.
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
let c: Color = Color.Green;
let colorName: string = Color[4]; // 'Blue'
unknown 类型是有type check的
declare const maybe: unknown;
// 'maybe' could be a string, object, boolean, undefined, or other types
const aNumber: number = maybe;
// Type 'unknown' is not assignable to type 'number'.
if (maybe === true) {
// TypeScript knows that maybe is a boolean now
const aBoolean: boolean = maybe;
// So, it cannot be a string
const aString: string = maybe;
// Type 'boolean' is not assignable to type 'string'.
}
If you have a variable with an unknown type, you can narrow it to something more specific by doing typeof
checks, comparison checks, or more advanced type guards that will be discussed.
any: opt-out type checking, it may propagate
declare function getValue(key: string): any;
// OK, return value of 'getValue' is not checked
const str: string = getValue("myString");
let looselyTyped: any = {};
let d = looselyTyped.a.b.c.d;
// ^ = let d: any
Remember that all the convenience of any
comes at the cost of losing type safety. Type safety is one of the main motivations for using TypeScript and you should try to avoid using any
when not necessary.
void 主要用于函数返回;用在变量上没什么意义
By default null
and undefined
are subtypes of all other types. That means you can assign null
and undefined
to something like number
.
However, when using the --strictNullChecks
flag, null
and undefined
are only assignable to unknown
, any
and their respective types (the one exception being that undefined
is also assignable to void
). This helps avoid many common errors. In cases where you want to pass in either a string
or null
or undefined
, you can use the union type string | null | undefined
.
The never
type represents the type of values that never occur. For instance, never
is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns. Variables also acquire the type never
when narrowed by any type guards that can never be true.
// Inferred return type is never
function fail() {
return error("Something failed");
}
// Function returning never must not have a reachable end point
function infiniteLoop(): never {
while (true) {}
}
The never
type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never
(except never
itself). Even any
isn’t assignable to never
.
object
declare function create(o: object | null): void;
Type assertions are a way to tell the compiler “trust me, I know what I’m doing.” A type assertion is like a type cast in other languages, but it performs no special checking or restructuring of data. It has no runtime impact and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.
Type assertions have two forms.
One is the as
-syntax:
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
The other version is the “angle-bracket” syntax:
let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;
The two samples are equivalent. Using one over the other is mostly a choice of preference; however, when using TypeScript with JSX, only as
-style assertions are allowed.
Both version cannot be used in destructuring assignment:
interface SlateNode{}
interface ListNode extends SlateNode{
type: string
}
type Path = number[]
type NodeEntry = [SlateNode, Path]
let entry: NodeEntry = [{ type: 'ul' } as SlateNode, [0,1]]
const [(node as ListNode)] = entry // error
console.log(node.type) // error
Possible solutions
const [node] = entry as [ListNode, Path] // ok
//or
console.log((node as ListNode).type) //ok
Interfaces
Interfaces are used to describe specific object types, including functions.
Structural SubTyping
optional properties
readonly: ReadonlyArrayreadonly
vs const
The easiest way to remember whether to use readonly
or const
is to ask whether you’re using it on a variable or a property. Variables use const
whereas properties use readonly
.
Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error:
function createSquare(config: SquareConfig): { color: string; area: number } {
return { color: config.color || "red", area: config.width ? config.width*config.width : 20 };
}
let mySquare = createSquare({ colour: "red", width: 100 });
Getting around these checks is actually really simple. The easiest method is to just use a type assertion:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
However, a better approach might be to add a string index signature if you’re sure that the object can have some extra properties that are used in some special way. If SquareConfig
can have color
and width
properties with the above types, but could also have any number of other properties, then we could define it like so:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
We’ll discuss index signatures in a bit, but here we’re saying a SquareConfig
can have any number of properties, and as long as they aren’t color
or width
, their types don’t matter.
One final way to get around these checks, which might be a bit surprising, is to assign the object to another variable: Since squareOptions
won’t undergo excess property checks, the compiler won’t give you an error.
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
The above workaround will work as long as you have a common property between squareOptions
and SquareConfig
. In this example, it was the property width
. It will however, fail if the variable does not have any common object property.
Function Types
To describe a function type with an interface, we give the interface a call signature. This is like a function declaration with only the parameter list and return type given. Each parameter in the parameter list requires both name and type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
// the names of the parameters do not need to match
let result = src.search(sub);
return result > -1;
};
// types can be inferred
let mySearch2: SearchFunc;
mySearch2 = function (src, sub) {
let result = src.search(sub);
return result > -1;
};
Indexable Types
Indexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing.
There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number
, JavaScript will actually convert that to a string
before indexing into an object. That means that indexing with 100
(a number
) is the same thing as indexing with "100"
(a string
), so the two need to be consistent.
While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type.
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
}
Class Types
When working with classes and interfaces, it helps to keep in mind that a class has two types: the type of the static side and the type of the instance side.
Extending Interfaces
An interface can extend multiple interfaces, creating a combination of all of the interfaces.
Hybrid Types
function object owning other attributes
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Interfaces Extending Classes
When an interface type extends a class type it inherits the members of the class but not their implementations. It is as if the interface had declared all of the members of the class without providing an implementation. Interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() {}
}
class TextBox extends Control {
select() {}
}
class ImageControl implements SelectableControl {
Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}
Functions
The return type can often be inferred
function foo(name: string, height: number){
if (height > 0){
return height
}else{
return name
}
}
let h:number = foo('gy', 100) // Type 'string | number' is not assignable to type 'number'
When writing out the whole function type, both parts are required. If the function doesn’t return a value, you would use void instead of leaving it off.
let myAdd: (baseValue: number, increment: number) => number = function (
x: number,
y: number
): number {
return x + y;
};
Of note, only the parameters and the return type make up the function type. Captured variables are not reflected in the type. In effect, captured variables are part of the “hidden state” of any function and do not make up its API.
Funciton types can be inferred:
// The parameters 'x' and 'y' have the type number
let myAdd = function (x: number, y: number): number {
return x + y;
};
// myAdd has the full function type
let myAdd2: (baseValue: number, increment: number) => number = function (x, y) {
return x + y;
};
This is called “contextual typing”, a form of type inference.
The number of arguments given to a function has to match the number of parameters the function expects. In JavaScript, every parameter is optional, and users may leave them off as they see fit. When they do, their value is undefined. We can get this functionality in TypeScript by adding a ? to the end of parameters we want to be optional.
Default-initialized parameters that come after all required parameters are treated as optional, and just like optional parameters, can be omitted when calling their respective function.
function buildName(firstName: string, lastName?: string) {
// ...
}
function buildName(firstName: string, lastName = "Smith") {
// ...
}
Both share the same type (firstName: string, lastName?: string) => string
If a default-initialized parameter comes before a required parameter, users need to explicitly pass undefined
to get the default initialized value.
Required, optional, and default parameters all have one thing in common: they talk about one parameter at a time. Sometimes, you want to work with multiple parameters as a group, or you may not know how many parameters a function will ultimately take. In JavaScript, you can work with the arguments directly using the arguments variable that is visible inside every function body.
In typescript, you can use rest parameters
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
// employeeName will be "Joseph Samuel Lucas MacKinzie"
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
Yehuda Katz’s Understanding JavaScript Function Invocation and “this”explains the inner workings of this
very well
Arrow functions capture the this
where the function is created rather than where it is invoked.
Even better, TypeScript will warn you when you make this mistake if you pass the --noImplicitThis
flag to the compiler.
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Unfortunately, the type of this.suits[pickedSuit]
is still any.
To fix this, you can provide an explicit this parameter. this parameters are fake parameters that come first in the parameter list of a function:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function (this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
You can also run into errors with this
parameter in callbacks. To solve it, first, the library author needs to annotate the callback type with this
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void
means that addClickListener
expects onclick
to be a function that does not require a this
type. Second, annotate your calling code with this
:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use `this` here because it's of type void!
console.log("clicked!");
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
Of course, this also means that it can’t use this.info. If you want both then you’ll have to use an arrow function:
class Handler {
info: string;
onClickGood = (e: Event) => {
this.info = e.message;
};
}
This works because arrow functions use the outer this, so you can always pass them to something that expects this: void
. The downside is that one arrow function is created per object of type Handler. Methods, on the other hand, are only created once and attached to Handler’s prototype. They are shared between all objects of type Handler.
overloads
It’s not uncommon for a single JavaScript function to return different types of objects based on the shape of the arguments passed in. How do we describe this to the type system?
The answer is to supply multiple function types for the same function as a list of overloads.
function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x: any): any {
//
}
In order for the compiler to pick the correct type check, it follows a similar process to the underlying JavaScript. It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.
Note that the function pickCard(x): any piece
is not part of the overload list, so it only has two overloads: one that takes an object and one that takes a number. Calling pickCard with any other parameter types would cause an error.
Literal Types
There are three sets of literal types available in TypeScript today: strings, numbers, and booleans; They can be mixed.
type t = 'hi' | 2 | false
Literal narrowing
// We're making a guarantee that this variable
// helloWorld will never change, by using const.
// So, TypeScript sets the type to be "Hello World" not string
const helloWorld = "Hello World";
// On the other hand, a let can change, and so the compiler declares it a string
let hiWorld = "Hi World";
The process of going from an infinite number of potential cases (there are an infinite number of possible string values) to a smaller, finite number of potential case is called narrowing.
Literal types can be used in the same way to distinguish overloads:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}
Unions and Intersection Types
A union type describes a value that can be one of several types.
Unions with Common Fields
If a value has the type A | B
, we only know for certain that it has members that both A
and B
have. (no matter whether they have common fields.)
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
declare function getSmallPet(): Fish | Bird;
let pet = getSmallPet();
pet.layEggs();
// error, only available in one of the two possible types
pet.swim();
Discriminating Unions
A common technique for working with unions is to have a single field which uses literal types which you can use to let TypeScript narrow down the possible current type. (Typescript will narrow it automatically)
type NetworkLoadingState = {
state: "loading";
};
type NetworkFailedState = {
state: "failed";
code: number;
};
type NetworkSuccessState = {
state: "success";
response: {
title: string;
duration: number;
summary: string;
};
};
// Create a type which represents only one of the above types
// but you aren't sure which it is yet.
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState;
function logger(state: NetworkState): string {
// By switching on state, TypeScript can narrow the union
state.code //error
// down in code flow analysis
switch (state.state) {
case "loading":
return "Downloading...";
case "failed":
// The type must be NetworkFailedState here,
// so accessing the `code` field is safe
return `Error ${state.code} downloading`;
case "success":
return `Downloaded ${state.response.title} - ${state.response.summary}`;
}
}
Intersection Types
An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.
interface ErrorHandling {
success: boolean;
error?: { message: string };
}
interface ArtworksData {
artworks: { title: string }[];
}
interface ArtistsData {
artists: { name: string }[];
}
// These interfaces are composed to have
// consistent error handling, and their own data.
type ArtworksResponse = ArtworksData & ErrorHandling;
type ArtistsResponse = ArtistsData & ErrorHandling;
const handleArtistsResponse = (response: ArtistsResponse) => {
if (response.error) {
console.error(response.error.message);
return;
}
console.log(response.artists);
};
union and intersection support one operand:
type A = | string // i.e. string
type B = & number // i.e. number
type C =
| string
| number