/** * (5) sometimes we need to declare a variable w/o initializing it */ let z; z = 41; z = "abc"; // (6) oh no! This isn't good
/** * If we look at the type of z, it's `any`. This is the most flexible type * in TypeScript (think of it like a JavaScript `let`) */
/** * (7) we could improve this situation by providing a type annotation * when we declare our variable */ letzz: number; zz = 41; zz = "abc"; // 🚨 ERROR Type '"abc"' is not assignable to type 'number'.
/** * (8) simple array types can be expressed using [] */ letaa: number[] = []; // 标识类型为数组,数组每个元素为数字类型 aa.push(33); aa.push("abc"); // 🚨 ERROR: Argument of type '"abc"' is not assignable to parameter of type 'number'.
/** * (9) we can even define a tuple, which has a fixed length */ letbb: [number, string, string, number] = [ 123, "Fake Street", "Nowhere, USA", 10110 ];
bb = [1, 2, 3]; // 🚨 ERROR: Type 'number' is not assignable to type 'string'.
/** * (10) Tuple values often require type annotations ( : [number, number] ) */ const xx = [32, 31]; // number[]; constyy: [number, number] = [32, 31];
/** * (11) object types can be expressed using {} and property names */ letcc: { houseNumber: number; streetName: string }; cc = { streetName: "Fake Street", houseNumber: 123 };
cc = { houseNumber: 33 }; /** * 🚨 Property 'streetName' * 🚨 is missing in type '{ houseNumber: number; }' * 🚨 but required in type '{ houseNumber: number; streetName: string; }'. */
/** * (12) You can use the optional operator (?) to * indicate that something may or may not be there */ letdd: { houseNumber: number; streetName?: string }; dd = { houseNumber: 33 };
// (13) if we want to re-use this type, we can create an interface interfaceAddress { houseNumber: number; streetName?: string; } // and refer to it by name letee: Address = { houseNumber: 33 };
letcontactInfo: HasEmail | HasPhoneNumber = Math.random() > 0.5 ? { // we can assign it to a HasPhoneNumber name: "Mike", phone: 3215551212 } : { // or a HasEmail name: "Mike", email: "mike@example.com" };
contactInfo.name; // NOTE: we can only access the .name property (the stuff HasPhoneNumber and HasEmail have in common)
/** * (15) Union types */ letotherContactInfo: HasEmail & HasPhoneNumber = { // we _must_ initialize it to a shape that's asssignable to HasEmail _and_ HasPhoneNumber name: "Mike", email: "mike@example.com", phone: 3215551212 };
otherContactInfo.name; // NOTE: we can access anything on _either_ type otherContactInfo.email; otherContactInfo.phone;
Type Systems & Object Shapes
Type Systems & Type Equivalence
1 2 3 4 5
functionvalidateInputField(input: HTMLInputElement) { /*...*/ } validateInputField(x); // can we regard x as an HTMLInputElement?
Nominal Type Systems answer this question based on whether x is an instance of a class/type named HTMLInputElement
Structural Type Systems only care about the shape of an object. This is how typescript works!
Object Shapes
When we talk about the shape of an object, we’re referring to the names of properties and types of their values
// (1) function arguments and return values can have type annotations functionsendEmail(to: HasEmail): { recipient: string; body: string } { return { recipient: `${to.name} <${to.email}>`, // Mike <mike@example.com> body: "You're pre-qualified for a loan!" }; }
// (2) or the arrow-function variant const sendTextMessage = ( to: HasPhoneNumber ): { recipient: string; body: string } => { return { recipient: `${to.name} <${to.phone}>`, body: "You're pre-qualified for a loan!" }; };
// (3) return types can almost always be inferred functiongetNameParts(contact: { name: string }) { const parts = contact.name.split(/\s/g); // split @ whitespace if (parts.length < 2) { thrownewError(`Can't calculate name parts from name "${contact.name}"`); } return { first: parts[0], middle: parts.length === 2 ? undefined : // everything except first and last parts.slice(1, parts.length - 2).join(" "), last: parts[parts.length - 1] }; }
// (4) rest params work just as you'd think. Type must be array-ish constsum = (...vals: number[]) => vals.reduce((sum, x) => sum + x, 0); console.log(sum(3, 4, 6)); // 13
// (5) we can even provide multiple function signatures // "overload signatures" functioncontactPeople(method: "email", ...people: HasEmail[]): void; functioncontactPeople(method: "phone", ...people: HasPhoneNumber[]): void;
/** * (6) they may be used in combination with other types */
// augment the existing PhoneNumberDict // i.e., imported it from a library, adding stuff to it interfacePhoneNumberDict { home: { /** * (7) interfaces are "open", meaning any declarations of the * - same name are merged */ areaCode: number; num: number; }; office: { areaCode: number; num: number; }; }
/** * (3) Access modifier keywords - "who can access this thing" * * - public - everyone * - protected - me and subclasses * - private - only me */
classParamPropContactimplementsHasEmail { constructor( public name: string, public email: string = "no email") { // nothing needed } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/** * (4) Class fields can have initializers (defaults) */ classOtherContactimplementsHasEmail, HasPhoneNumber { protectedage: number = 0; // private password: string; constructor( public name: string, public email: string, public phone: number) { // () password must either be initialized like this, or have a default value this.password = Math.round(Math.random() * 1e14).toString(32); } }
Definite Assignment & Lazy Initalization
1 2 3 4 5 6 7 8 9 10 11
/** * (4) Class fields can have initializers (defaults) */ classOtherContactimplementsHasEmail, HasPhoneNumber { protectedage: number = 0; privatepassword: string; constructor(public name: string, public email: string, public phone: number) { // () password must either be initialized like this, or have a default value this.password = Math.round(Math.random() * 1e14).toString(32); } }
Abstract Classes
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/** * (5) TypeScript even allows for abstract classes, which have a partial implementation */
abstractclassAbstractContactimplementsHasEmail, HasPhoneNumber { publicabstractphone: number; // must be implemented by non-abstract subclasses
constructor( public name: string, public email: string// must be public to satisfy HasEmail ) {}
abstractsendEmail(): void; // must be implemented by non-abstract subclasses }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/** * (6) implementors must "fill in" any abstract methods or properties */ classConcreteContactextendsAbstractContact { constructor( public phone: number, // must happen before non property-parameter arguments name: string, email: string ) { super(name, email); } sendEmail() { // mandatory! console.log("sending an email"); } }
Converting to TypeScript
What not to do
Functional changes at the same time
Attempt this with low test coverage
Let the perfect be the enemy of the good
Forget to add tests for your types
Publish types for consumer use while they’re in a “weak” state
Compliling in “loose mode”
Start with tests passing
Rename all .js to .ts, allowing implicit any
Fix only things that are not type-checking, or causing compile errors
Be careful to avoid changing behavior
Get tests passing again
Making Anys Explicit
Start with tests passing
Ban implicit any("noImplicitAny: true,)
Where possible, provide a specific and appropriate type
Import types for dependencies from DefinitelyTyped
/** * we can name these params whatever we want, but a common convention * is to use capital letters starting with `T` (a C++ convention from "templates") */
/** * (2) Type parameters can have default types * - just like function parameters can have default values */
conststringFilter: FilterFunction<string> = val =>typeof val === "string"; stringFilter(0); // 🚨 ERROR stringFilter("abc"); // ✅ OK
// can be used with any value consttruthyFilter: FilterFunction = val => val; truthyFilter(0); // false truthyFilter(1); // true truthyFilter(""); // false truthyFilter(["abc"]); // true
/** * (6) When to use generics * * - Generics are necessary when we want to describe a relationship between * - two or more types (i.e., a function argument and return type). * * - aside from interfaces and type aliases, If a type parameter is used only once * - it can probably be eliminated */
/** * (1) "Top types" are types that can hold any value. Typescript has two of them */
letmyAny: any = 32 letmyUnknown: unknown = 'hello, unknown'
// Note that we can do whatever we want with an any, but nothing with an unknown
myAny.foo.bar.baz myUnknown.foo
/** * (2) When to use `any` * Anys are good for areas of our programs where we want maximum flexibility * Example: sometimes a Promise<any> is fine when we don't care at all about the resolved value */ asyncfunctionlogWhenResolved(p: Promise<any>) { const val = await p console.log('Resolved to: ', val) }
/** * (3) When to use `unknown` * Unknowns are good for "private" values that we don't want to expose through a public API. * They can still hold any value, we just must narrow the type before we're able to use it. * * We'll do htis with a type guard. */
/** * (4) Built-in type guards */ if (typeof myUnknown === "string") { // in here, myUnknown is of type string myUnknown.split(", "); // ✅ OK } if (myUnknown instanceofPromise) { // in here, myUnknown is of type Promise<any> myUnknown.then(x =>console.log(x)); }
/** * (5) User-defined type guards * We can also create our own type guards, using functions that return booleans */
// 💡 Note return type functionisHasEmail(x: any): x is HasEmail { returntypeof x.name === "string" && typeof x.email === "string"; }
if (isHasEmail(myUnknown)) { // In here, myUnknown is of type HasEmail console.log(myUnknown.name, myUnknown.email); }
// my most common guard function isDefined<T>(arg: T | undefined): arg is T { returntypeof arg !== "undefined"; }
/** * (6) Dealing with multiple unknowns * - We kind of lose some of the benefits of structural typing when using `unknown`. * - Look how we can get mixed up below */
/** * (7) Alternative to unknowns - branded types * - One thing we can do to avoid this is to create types with structures that * - are difficult to accidentally match. This involves unsafe casting, but it's ok * - if we do things carefully */
/* two branded types, each with "brand" and "unbrand" functions */ interfaceBrandedA { __this_is_branded_with_a: "a"; } functionbrandA(value: string): BrandedA { return (value asunknown) asBrandedA; } functionunbrandA(value: BrandedA): string { return (value asunknown) asstring; }
/** * (8) Bottom types can hold no values. TypeScript has one of these: `never` */ letn: never = 4;
/** * A common place where you'll end up with a never * is through narrowing exhaustively */
let x = "abc"asstring | number;
if (typeof x === "string") { // x is a string here x.split(", "); } elseif (typeof x === "number") { // x is a number here x.toFixed(2); } else { // x is a never here }
/** * (9) We can use this to our advantage to create exhaustive conditionals and switches */
classUnreachableErrorextendsError { constructor(val: never, message: string) { super(`TypeScript thought we could never end up here\n${message}`); } }
let y = 4asstring | number;
if (typeof y === "string") { // y is a string here y.split(", "); } elseif (typeof y === "number") { // y is a number here y.toFixed(2); } else { thrownewUnreachableError(y, "y should be a string or number"); }
// we can get all values by mapping through all keys typeAllCommKeys = keyof CommunicationMethods typeAllCommValues = CommunicationMethods[keyof CommunicationMethods]
/** * (2) Type queries allow us to obtain the type from a value using typeof */
/** * (3) Conditional types allow for the use of a ternary operator w/ types * We can also extract type parameters using the _infer_ keyword */
typeEventualType<T> = T extendsPromise<infer S> // if T extends Promise<any> ? S // extract the type the promise resolves to : T; // otherwise just let T pass through
leta: EventualType<Promise<number>>; letb: EventualType<number[]>;
/** * (4) Partial allows us to make all properties on an object optional */ typeMayHaveEmail = Partial<HasEmail>; constme: MayHaveEmail = {}; // everything is optional
/** * (5) Pick allows us to select one or more properties from an object type */
/** * (7) Exclude lets us obtain a subset of types that are NOT assignable to something */ typeNotStrings = Exclude<"a" | "b" | 1 | 2, string>;
/** * (8) Record helps us create a type with specified property keys and the same value type */ typeABCPromises = Record<"a" | "b" | "c", Promise<any>>;
const contactClass = Contact; // value relates to the factory for creating instances constcontactInstance: Contact = newContact(); // interface relates to instances
/** * (5) declarations with the same name can be merged, to occupy the same identifier */
import * as path from'path' import * as ts from'typescript'
function isDefined<T>(x: T | undefined): x is T { returntypeof x !== 'undefined' }
// (1) Create the program const program = ts.createProgram({ options: { target: ts.ScriptTarget.ESNext }, rootNames: [ // path to ../examples/hello-ts/src/index.ts path.join(__dirname, '..', 'examples', 'hello-ts', 'src', 'index.ts') ] })
// (2) Get the non-declaration (.d.ts) source files (.ts) const nonDeclFiles = program .getSourceFiles() .filter(sf => !sf.isDeclarationFile)
// (3) get the type-checker const checker = program.getTypeChecker()
/** * (4) use the type checker to obtain the * - appropriate ts.Symbol for each SourceFile */ const sfSymbols = nonDeclFiles .map(f => checker.getSymbolAtLocation(f)) .filter(isDefined) // here's the type guard to filter out undefined
// (5) for each SourceFile Symbol sfSymbols.forEach(sfSymbol => { const { exports: fileExports } = sfSymbol console.log(sfSymbol.name) if (fileExports) { // - if there are exports console.log('== Exports ==') fileExports.forEach((value, key) => { // - for each export console.log( key, // - log its name
// - and its type (stringified) checker.typeToString(checker.getTypeAtLocation(value.valueDeclaration)) ) const jsDocTags = value.getJsDocTags() if (jsDocTags.length > 0) { // - if there are JSDoc comment tags console.log( // - log them out as key-value pairs jsDocTags.map(tag =>`\t${tag.name}: ${tag.text}`).join('\n') ) } }) } })