Last active
July 19, 2019 17:25
-
-
Save greglockwood/77b70c961421e459c2e054794f3291e6 to your computer and use it in GitHub Desktop.
NoExtraProperties<T> generic type in TypeScript
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
node_modules | |
**/*.js |
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
import { Impossible, NoExtraProperties } from './no-extra-properties'; | |
// Now let's try it out! | |
// A simple type to work with | |
interface Animal { | |
name: string; | |
noise: string; | |
} | |
// This works, but I agree the type is pretty gross. But it might make it easier to see how this works. | |
// Whatever is passed to the function has to at least satisfy the Animal contract (the <T extends Animal> part), | |
// but then we intersect whatever type that is with an Impossible type which has only the keys on it that | |
// don't exist on Animal. The result is that the keys that don't exist on Animal have a type of `never`, | |
// so if they exist, they get flagged as an error! | |
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void { | |
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`); | |
} | |
// This is the best I could reduce it to, using the NoExtraProperties<> type above. | |
// Functions which use this technique will need to all follow this formula. | |
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void { | |
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`); | |
} | |
// It works for variables defined as the type | |
const okay: NoExtraProperties<Animal> = { | |
name: 'Dog', | |
noise: 'bark', | |
}; | |
const wrong1: NoExtraProperties<Animal> = { | |
name: 'Cat', | |
noise: 'meow' | |
betterThanDogs: false, // look, an error! | |
}; | |
// What happens if we try to bypass the "Excess Properties Check" done on object literals (https://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks) | |
// by assigning it to a variable with no explicit type? | |
const wrong2 = { | |
name: 'Rat', | |
noise: 'squeak', | |
idealScenarios: ['labs', 'storehouses'], | |
invalid: true, | |
}; | |
thisWorks(okay); | |
thisWorks(wrong1); // doesn't flag it as an error here, but does flag it above | |
thisWorks(wrong2); // yay, an error! | |
thisIsAsGoodAsICanGetIt(okay); | |
thisIsAsGoodAsICanGetIt(wrong1); // no error, but error above, so okay | |
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error! |
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
// First, define a type that, when passed a union of keys, creates an object which cannot have those properties. | |
// I couldn't find a way to use this type directly, but it can be used with the below type. | |
export type Impossible<K extends keyof any> = { | |
[P in K]: never; | |
}; | |
// The secret sauce! Provide it the type that contains only the properties you want, and then a type that extends that type, | |
// based on what the caller provided using generics. | |
export type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>; |
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
{ | |
"name": "no-extra-properties", | |
"version": "1.0.0", | |
"lockfileVersion": 1, | |
"requires": true, | |
"dependencies": { | |
"typescript": { | |
"version": "3.5.3", | |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", | |
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", | |
"dev": true | |
} | |
} | |
} |
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
{ | |
"name": "no-extra-properties", | |
"version": "1.0.0", | |
"description": "NoExtraProperties<> generic type in TypeScript", | |
"main": "no-extra-properties.d.ts", | |
"scripts": { | |
"test": "tsc demo.ts", | |
"start": "tsc demo.ts" | |
}, | |
"repository": { | |
"type": "git", | |
"url": "git+https://gist.github.com/77b70c961421e459c2e054794f3291e6.git" | |
}, | |
"author": "Greg Lockwood <greg.lockwood.is@gmail.com>", | |
"license": "MIT", | |
"bugs": { | |
"url": "https://gist.github.com/77b70c961421e459c2e054794f3291e6" | |
}, | |
"homepage": "https://gist.github.com/77b70c961421e459c2e054794f3291e6", | |
"devDependencies": { | |
"typescript": "^3.5.3" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment