TypeScript is a highly configurable language.
It comes with over a hundred compiler options that can be provided via the command-line to tsc and/or in a "TSConfig" configuration file (by default, tsconfig.json).
tip
TypeScript's compiler options are documented at aka.ms/tsconfig.
compilerOptions.target in particular can be an important configuration option for your project.
It specifies which ECMAScript version your project's output JavaScript code must support.
You can specify target in your TSConfig as the string name of an ECMAScript version, such as "es5"or"es2021":
jsonc
// tsconfig.json
{
"compilerOptions": {
"target":"es2021"
}
}
jsonc
// tsconfig.json
{
"compilerOptions":{
"target":"es2021"
}
}
This article explores what target influences and why that's useful.
Let's dig in!
TypeScript 4.9 introduces a new operator, satisfies, that allows opting into a different kind of type inference from the type system's default.
satisfies brings the best of type annotations and default type inference together in a useful manner.
Let's explore the new satisfies operator and why it's useful!
TypeScript's default type inference for objects defaults to inferring primitive values for types, rather than literals.
For example, the vibe.mood property in the following value is string, not "happy":
ts
constvibe= {
mood:"happy",
(property) mood: string
};
ts
constvibe={
mood:"happy",
(property) mood: string
};
TypeScript's default inference is not always optimal since you might want to have a different type.
For our vibe.mood, we might have wanted "happy" - or maybe even a union of string literals, such as "happy" | "sad".
You can use a : type annotation to specify a variable's type.
Doing so tells TypeScript to use the annotated type for the variable instead of its default inference.
For example, using a : Vibe type annotation on the vibe variable here switches its mood property from string to "happy" | "sad":
ts
interfaceVibe {
mood:"happy"|"sad";
}
constvibe:Vibe= {
mood:"happy",
(property) Vibe.mood: "happy" | "sad"
};
vibe.mood;
(property) Vibe.mood: "happy" | "sad"
ts
interfaceVibe{
mood:"happy"|"sad";
}
constvibe:Vibe={
mood:"happy",
(property) Vibe.mood: "happy" | "sad"
};
vibe.mood;
(property) Vibe.mood: "happy" | "sad"
But, this has a flaw too!
See the type of vibe.mood after the variable declaration.
As developers reading the code, we know vibe.mood should be the more specific ("narrow") "happy", not the more general ("wide") "happy" | "sad".
as const type assertions can also be used to modify types.
But those add readonly to all array and object types, which also might not be what you want.
TypeScript 4.9's new satisfies operator introduces a happy compromise between : annotations and the default type inference.
The syntax for satisfies is to place it after a value and before a name of a type:
ts
someValue satisfies SomeType;
ts
someValuesatisfiesSomeType;
satisfies applies the best of both worlds:
The value must adhere to a specific shape (as with : declarations )
Type inference is still allowed to give the value a more narrow shape than the declared type
In other words, satisfies makes sure a value's type matches some type shape, but doesn't widen the type unnecessarily.
We can use satisfies on our vibe object to make sure its mood property allowed to be only is "happy" | "sad", but is still only "happy" afterwards:
ts
interfaceVibe {
mood:"happy"|"sad";
}
constvibe= {
mood:"happy",
(property) Vibe.mood: "happy" | "sad"
} satisfies Vibe;
vibe.mood;
(property) mood: "happy"
ts
interfaceVibe{
mood:"happy"|"sad";
}
constvibe={
mood:"happy",
(property) Vibe.mood: "happy" | "sad"
}satisfiesVibe;
vibe.mood;
(property) mood: "happy"
By using satisfies, we were able to make sure our vibe matched the Vibe interface, without forcing too wide a type for the mood property.
The Next.js framework provides an excellent example of how satisfies improves the coding experience for TypeScript developers.
Its data fetching patterns such as getServerSideProps and getStaticProps allow users to declare functions with those names in order to retrieve data for a component.
A simplified version of the GetServerSideProps type might look like:
ts
exporttypeGetServerSideProps<Props> = (
context:GetServerSidePropsContext
) =>Promise<GetServerSidePropsResult<Props>>;
exportinterfaceGetServerSidePropsContext {
query:Record<string,string>;
}
exporttypeGetServerSidePropsResult<P> =
| { props:P|Promise<P> }
| { notFound:true };
ts
exporttypeGetServerSideProps<Props>= (
context:GetServerSidePropsContext
) =>Promise<GetServerSidePropsResult<Props>>;
exportinterfaceGetServerSidePropsContext{
query:Record<string,string>;
}
exporttypeGetServerSidePropsResult<P>=
|{props:P|Promise<P>}
|{notFound:true};
Before satisfies, there was no good way to declare many generic getServerSideProps functions without using explicit type annotations.
Code could use an explicit generic : GetServerSideProps<...> type annotation, but then that would need to be explicitly told a function type parameter for the generic Props type argument:
Leaving out the : GetServerSideProps<...> type annotation meant the getServerSideProps function wouldn't be inferred to satisfy that type.
Its context parameter would be implicitly type any without a type annotation.
Even if context were to be removed or given a proper : ServerSidePropsContext type annotation, nothing would enforce the function has the right return type:
By adding satisfies GetServerSideProps after our function, we were able to enforce that it adheres to the proper Next.js interface -- while still allowing it to infer a generic type for its props.
Hooray! 🎉
The satisfies operator was designed and released after the Learning TypeScript book's contents were finalized.
satisfies is not mentioned in Learning TypeScript.
Still, Learning TypeScript's The Type System chapter teaches all the concepts you'll need to be able to understand TypeScript's type system.
That includes how the type system works, the default type inferences mentioned in this article, and explicitly using : type annotations.
Lastly, appreciation to Oleksandr Tarasiuk who implemented and iterated on the feature with the TypeScript team.
Oleksandr is by far the most profilic community contributor to TypeScript and deserves much gratitude and praise.
💙
TypeScript's type narrowing is a powerful feature of TypeScript's type system that lets it infer more specific types for values in areas of code.
For example, TypeScript would understand that inside the following if statement, the fruit variable has to be the literal value "apple":
ts
constfruit=Math.random() >0.5?"apple":undefined;
fruit;
const fruit: "apple" | undefined
if (fruit) {
fruit;
const fruit: "apple"
}
ts
constfruit=Math.random() >0.5?"apple":undefined;
fruit;
const fruit: "apple" | undefined
if (fruit) {
fruit;
const fruit: "apple"
}
But, TypeScript's type system isn't perfect.
There are some cases where TypeScript can't narrow types exactly the way you might want.
Take a look at this code snippet, where counts.apple is inferred to be type number:
ts
constcounts= {
apple:1,
};
counts.apple;
(property) apple: number
ts
constcounts={
apple:1,
};
counts.apple;
(property) apple: number
While counts is type { apple: number }, shouldn't TypeScript know that the immediately available value of counts.apple is specifically the literal type 1 and not the general primitive type number?
Can't TypeScript tell we haven't changed the value yet?
TypeScript's type system is Turing Complete: meaning it has conditional branching (conditional types) and works with an arbitrary huge amount of memory.
As a result, you can use the type system as its own programming language complete with variables, functions, and recursion.
Developers have pushed the bounds of type operations possible in the type system to write some pretty incredible things!
This blog post is a starting list of nifty things TypeScript developers have pushed the type system to be able to do.
They range from binary arithmetic and rudimentary virtual machines to maze solvers and full programming languages.
Most applications try to get away with as few extreme type operations as possible.
Complex logic in the type system gets unreadable and hard to debug pretty quickly.
The Learning TypeScript book advises:
If you do find a need to use type operations, please—for the sake of any developer who has to read your code, including a future you—try to keep them to a minimum if possible.
Use readable names that help readers understand the code as they read it.
Leave descriptive comments for anything you think future readers might struggle with.
Please don't look at the following projects list and think you need to understand them to use TypeScript.
These projects are ridiculous.
They're the equivalent of code golf: a fun activity for a select few, but not useful for most day-to-day work.
Most popular programming languages use the void keyword to indicate that a function cannot return a value.
Learning TypeScript describes TypeScript's void keyword as indicating that the returned value from a function will be ignored.
Those two definitions are not always the same!
TypeScript's void comes with an initially surprising behavior: a function type with a non-void return is considered assignable to a function type with a void return.
Finding the right names and values for small code snippets is a surprisingly challenging task.
Sample content like foo bar and lorem ipsum gets boring quickly -- and can be confusing to readers who aren't yet familiar with them.
It's better to have self-contained code snippets that can be clearly understood without any external context.
Crafting appropriate themed content that fits cleanly with the theory being demonstrated is fraught with danger.
Code snippets shouldn't contain an egregious quantity of tangentially-related setup code just to justify their theme.
Themes also shouldn't be unnecessarily flashy to the point of distracting from the important conceptual explanations.
Many authors opt to stick with a small set of themes they know to be flexible and work well.
I personally go with names of fruit, such as counting amounts of them.
While sticking with the same theme repeatedly is reliable, it can get boring for the reader fast.
That's why, at risk of distracting ever so slightly from code content, most chapters in Learning TypeScript use an overarching theme for most or all of its code snippets.
Most of the themes are fairly straightforward, such as historical authors or inventors.
Others are a little more subtle and act more like easter eggs.