Type Compatibility Flashcards
What is Structural Typing in TypeScript?
Structural typing is a way of relating types based solely on their members. TypeScript uses structural typing for checking the compatibility and assignment between different types. This means that if two objects have the same structure (same properties and methods with the same types), TypeScript considers these objects to be of the same type, even if their named types are different or not present.
interface Pet { name: string; } class Dog { name: string; } let pet: Pet; // OK, because of structural typing pet = new Dog();
“Documentation - Type Compatibility” (typescriptlang.org). Retrieved July 25, 2023.
What are the “excess property checks” of object literals in TypeScript?
Excess property checks in TypeScript are a part of the type-checking process where the compiler checks the properties of object literals against the type they are being assigned to. If any properties do not exist on the target type, TypeScript will raise an error.
interface Pet { name: string; } let dog: Pet = { name: "Lassie", owner: "Rudd Weatherwax" }; // Error
You could argue that this program is correctly typed, since the name
properties are compatible.
However, TypeScript takes the stance that there’s probably a bug in this code. 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:
“Excess Property Checks” (typescriptlang.org). Retrieved July 25, 2023.
How can you bypass “excess property checks” of object literals?
There are several ways to bypass these checks in TypeScript:
For example given:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { return { color: config.color || "red", area: config.width ? config.width * config.width : 20, }; }
You could bypass the excess property checks by:
- Using a type assertion:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
- Adding a string index signature to the interface to allow additional properties:
interface SquareConfig { color?: string; width?: number; [propName: string]: any; }
Assigning the object to another variable before passing it as an argument, as long as there is at least one common property:
let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
“Excess Property Checks” (typescriptlang.org). Retrieved July 25, 2023.
What is the rule for assigning functions based on their parameter lists?
In TypeScript, each parameter in the source function must have a corresponding parameter in the target function with a compatible type. The names of the parameters are not considered, only their types. A function with fewer parameters can be assigned to a function with more parameters. This is because ignoring extra function parameters is common in JavaScript.
Example:
let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // OK x = y; // Error, because x lacks the second parameter that y has
“Comparing two functions” (typescriptlang.org). Retrieved July 26, 2023.
Why does TypeScript allow ‘discarding’ parameters when assigning functions?
Ignoring extra function parameters is actually quite common in JavaScript. For example, Array
’s forEach
method provides three parameters to the callback function: the array element, its index, and the containing array. But, it’s often useful to provide a callback that only uses the first parameter.
Example:
let items = [1, 2, 3]; // It's OK to ignore index and array parameters items.forEach((item) => console.log(item));
“Comparing two functions” (typescriptlang.org). Retrieved July 26, 2023.
What is function parameter bivariance in TypeScript?
Function parameter bivariance in TypeScript refers to the situation where assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. This could potentially be unsound because a caller might end up being given a function that takes a more specialized type, but the function could be invoked with a less specialized type. However, this is allowed as it enables many common JavaScript patterns.
Example:
enum EventType { Mouse, Keyboard, } interface Event { timestamp: number; } interface MyMouseEvent extends Event { x: number; y: number; } interface MyKeyEvent extends Event { keyCode: number; } function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */ } // Unsound, but common and useful listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y)); // Undesirable alternatives in presence of soundness listenEvent(EventType.Mouse, (e: Event) => console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y) ); listenEvent(EventType.Mouse, ((e: MyMouseEvent) => console.log(e.x + "," + e.y)) as (e: Event) => void); // Still disallowed (clear error). Type safety enforced for wholly incompatible types listenEvent(EventType.Mouse, (e: number) => console.log(e));
“Function Parameter Bivariance” (typescriptlang.org). Retrieved July 26, 2023.
How can you make TypeScript raise errors when function parameter bivariance occurs?
TypeScript can raise errors when function parameter bivariance occurs by using the compiler flag strictFunctionTypes
. When this flag is enabled, it enforces stricter checking of function parameter types to prevent potentially unsound assignments.
Example:
// Enable this flag in tsconfig.json or on the command line { "compilerOptions": { "strictFunctionTypes": true } }
In this mode, the unsound but common use of function parameter bivariance would cause a compiler error.
“Function Parameter Bivariance” (typescriptlang.org). Retrieved July 26, 2023.
enums
type compatibility
enums
are compatible with numbers
, and numbers
are compatible with enums
. Enum values from different enum
types are considered incompatible. For example,
enum Status { Ready, Waiting, } enum Color { Red, Blue, Green, } let status = Status.Ready; status = Color.Green; // Error
“Enums” (typescriptlang.org). Retrieved July 31, 2023.
Classes type compatibility
Classes work similarly to object literal types and interfaces with one exception: they have both a static and an instance type. When comparing two objects of a class type, only members of the instance are compared. Static members and constructors do not affect compatibility.
class Animal { feet: number; constructor(name: string, numFeet: number) {} } class Size { feet: number; constructor(numFeet: number) {} } let a: Animal; let s: Size; a = s; // OK s = a; // OK
“Classes” (typescriptlang.org). Retrieved July 31, 2023.
Do private and protected members affect the type compatibility of classes?
Yes. Private and protected members in a class affect their compatibility. When an instance of a class is checked for compatibility, if the target type contains a private member, then the source type must also contain a private member that originated from the same class. Likewise, the same applies for an instance with a protected member. This allows a class to be assignment compatible with its super class, but not with classes from a different inheritance hierarchy which otherwise have the same shape.
“Classes” (typescriptlang.org). Retrieved July 31, 2023.
Generics type compatibility
Because TypeScript is a structural type system, type parameters only affect the resulting type when consumed as part of the type of a member. For example,
interface Empty<T> {} let x: Empty<number>; let y: Empty<string>; x = y; // OK, because y matches structure of x
In the above, x
and y
are compatible because their structures do not use the type argument in a differentiating way. Changing this example by adding a member to Empty<T>
shows how this works:
interface NotEmpty<T> { data: T; } let x: NotEmpty<number>; let y: NotEmpty<string>; x = y; // Error, because x and y are not compatible
For generic types that do not have their type arguments specified, compatibility is checked by specifying any in place of all unspecified type arguments. The resulting types are then checked for compatibility, just as in the non-generic case.
For example,
let identity = function <T>(x: T): T { // ... }; let reverse = function <U>(y: U): U { // ... }; identity = reverse; // OK, because (x: any) => any matches (y: any) => any
“Generics” (typescriptlang.org). Retrieved July 31, 2023.