Last active
July 18, 2023 20:43
-
-
Save marcogrcr/344b32d3925947c96ba99c7a4fb7aaa2 to your computer and use it in GitHub Desktop.
TypeScript unexpected type checks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Even with the strictest settings, the TypeScript compiler may have looser type checking than what you would expect. | |
interface Data { | |
req1: number; | |
req2: number; | |
opt1?: number; | |
opt2?: number; | |
} | |
function logData(data: Data): void { | |
console.log(data); | |
} | |
// | |
// IMPLICIT TYPE DECLARATION | |
// As expected, required properties are checked: | |
// > Argument of type '{}' is not assignable to parameter of type 'Data'. | |
// > Type '{}' is missing the following properties from type 'Data': req1, req2 (2345) | |
logData({}); | |
// As expected, partial required properties are not enough: | |
// > Argument of type '{ req1: number; }' is not assignable to parameter of type 'Data'. | |
// > Property 'req2' is missing in type '{ req1: number; }' but required in type 'Data'. (2345) | |
logData({ req1: 1 }); | |
// Even if required properties are present, extraneous properties are not allowed: | |
// > Argument of type '{ req1: number; req2: number; opt3: number; }' is not assignable to parameter of type 'Data'. | |
// > Object literal may only specify known properties, and 'opt3' does not exist in type 'Data'. (2345) | |
logData({ req1: 1, req2: 2, opt3: 3 }); | |
// | |
// EXPLICIT TYPE DECLARATION | |
// The same applies in const/let/var declarations | |
// > Type '{}' is missing the following properties from type 'Data': req1, req2 (2739) | |
var d1: Data = {}; | |
let d2: Data = {}; | |
const d3: Data = {}; | |
// > Property 'req2' is missing in type '{ req1: number; }' but required in type 'Data'. (2741) | |
var d4: Data = { req1: 1 }; | |
let d5: Data = { req1: 1 }; | |
const d6: Data = { req1: 1 }; | |
// > Type '{ req1: number; req2: number; opt3: number; }' is not assignable to type 'Data'. | |
// > Object literal may only specify known properties, and 'opt3' does not exist in type 'Data'.(2322) | |
var d7: Data = { req1: 1, req2: 2, opt3: 3 }; | |
let d8: Data = { req1: 1, req2: 2, opt3: 3 }; | |
const d9: Data = { req1: 1, req2: 2, opt3: 3 }; | |
// | |
// IMPLICIT CASTING | |
// Due to TypeScript's "duck typing", it will happily allow extraneous properties as long as it's first another type and it overlaps on all required properties. | |
// This can be a source of bugs when there are typos on optional properties. | |
// This can be avoided with implicit or explicit type declaration, as shown on the previous examples. | |
const d10 = { req1: 1, req2: 2, opt3: 3 }; | |
logData(d10); | |
// | |
// EXPLICIT CASTING | |
// Explicit casting, will disallow types with extraneous properties if there's no overlap on all required properties. | |
// > Conversion of type '{ opt3: number; }' to type 'Data' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. | |
// > Type '{ opt3: number; }' is missing the following properties from type 'Data': req1, req2 (2352) | |
logData({ opt3: 3 } as Data); | |
// > Conversion of type '{ req1: number; opt3: number; }' to type 'Data' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. | |
// > Property 'req2' is missing in type '{ req1: number; opt3: number; }' but required in type 'Data'.(2352) | |
logData({ req1: 1, opt3: 3 } as Data); | |
// However, explicit casting will happily allow extraneous properties if all the required properties are present | |
// Just like with "duck typing", this can be a source of bugs when there are typos on optional properties. | |
// This can be avoided by removing the explicit casting if possible. | |
logData({ req1: 1, req2: 2, opt3: 3 } as Data); | |
// Finally, explicit casting will happily accept objects with partial or no overlap in the required properties, as long as there are no extraneous properties | |
// This can be a source of bugs and unexpected `TypeError: Cannot read properties of undefined` given the expectation that required properties will always have a value. | |
// This can be avoided by removing the explicit casting if possible. | |
logData({} as Data); | |
logData({ req1: 1 } as Data); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment