type World = "world";
type Greeting = `hello ${World}`;
// ^ = type Greeting = "hello world"
We can do shit like this:
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
// Takes
// | "top-left" | "top-center" | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;
setAlignment("top-left"); // works!
setAlignment("top-middel"); // error!
Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle
Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
Some of the real value comes from dynamically creating new string literals. For example, imagine a makeWatchedObject API that takes an object and produces a mostly identical object, but with a new on method to detect for changes to the properties.
let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});
person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});
type PropEventSource<T> = {
on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
// error!
person.on("firstName", () => {});
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
Uppercase
$UpperCase<T>, Lowercase, Capitalize, Uncapitalize type aliases.
Mapped types
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
You can re-map keys in mapped types with new `as` clause.
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^ = type KindlessCircle = {
// radius: number;
// }
Recursive conditional - skip
Indexed accesses with flag `--noUncheckedIndexedAccess`. With this on, you have to explicitly tell the TS compiler that you're accessing a value in a key that might be nil.
interface Options {
path: string;
permissions: number;
// Extra properties are caught by this index signature.
[propName: string]: string | number;
}
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// These are not allowed with noUncheckedIndexedAccess
opts.yadda.toString();
Object is possibly 'undefined'.
opts["foo bar baz"].toString();
Object is possibly 'undefined'.
opts[Math.random()].toString();
Object is possibly 'undefined'.
// Checking if it's really there first.
if (opts.yadda) {
console.log(opts.yadda.toString());
}
// Basically saying "trust me I know what I'm doing"
// with the '!' non-null assertion operator.
opts.yadda!.toString();
}
Path-mapping - in TS 4.1, the `paths` option can be used without `baseUrl`.